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, Vladimir Spivak (cwolf3d)",
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
34 from bpy_extras
import object_utils
37 from mathutils
import Matrix
, Vector
38 from mathutils
.geometry
import (
47 from bpy
.props
import (
56 from bpy
.types
import (
63 # ----------------------------
65 global_shade_smooth
= False
66 global_mesh_object
= ""
67 global_gpencil_object
= ""
68 global_curve_object
= ""
70 # ----------------------------
72 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
73 bl_space_type
= 'VIEW_3D'
76 bl_label
= "Bsurfaces"
78 def draw(self
, context
):
80 bs
= context
.scene
.bsurfaces
82 col
= layout
.column(align
=True)
85 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
86 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
88 col
.label(text
="Mesh of BSurface:")
89 col
.prop(bs
, "SURFSK_mesh", text
="")
90 if bs
.SURFSK_mesh
!= None:
91 try: mesh_object
= bs
.SURFSK_mesh
93 try: col
.prop(mesh_object
.data
.materials
[0], "diffuse_color")
95 try: col
.prop(mesh_object
.modifiers
['Shrinkwrap'], "offset")
97 try: col
.prop(mesh_object
, "show_in_front")
99 try: col
.prop(bs
, "SURFSK_shade_smooth")
101 try: col
.prop(mesh_object
, "show_wire")
104 col
.label(text
="Guide strokes:")
105 col
.row().prop(bs
, "SURFSK_guide", expand
=True)
106 if bs
.SURFSK_guide
== 'GPencil':
107 col
.prop(bs
, "SURFSK_gpencil", text
="")
109 if bs
.SURFSK_guide
== 'Curve':
110 col
.prop(bs
, "SURFSK_curve", text
="")
114 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
115 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
118 if bs
.SURFSK_guide
== 'GPencil':
119 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
120 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
122 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
124 if bs
.SURFSK_guide
== 'Annotation':
125 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
127 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
129 if bs
.SURFSK_guide
== 'Curve':
130 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
133 col
.label(text
="Initial settings:")
134 col
.prop(bs
, "SURFSK_edges_U")
135 col
.prop(bs
, "SURFSK_edges_V")
136 col
.prop(bs
, "SURFSK_cyclic_cross")
137 col
.prop(bs
, "SURFSK_cyclic_follow")
138 col
.prop(bs
, "SURFSK_loops_on_strokes")
139 col
.prop(bs
, "SURFSK_automatic_join")
140 col
.prop(bs
, "SURFSK_keep_strokes")
142 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
143 bl_space_type
= 'VIEW_3D'
144 bl_region_type
= 'UI'
145 bl_context
= "curve_edit"
147 bl_label
= "Bsurfaces"
150 def poll(cls
, context
):
151 return context
.active_object
153 def draw(self
, context
):
156 col
= layout
.column(align
=True)
159 col
.operator("curve.surfsk_first_points", text
="Set First Points")
160 col
.operator("curve.switch_direction", text
="Switch Direction")
161 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
164 # ----------------------------
165 # Returns the type of strokes used
166 def get_strokes_type(context
):
167 strokes_type
= "NO_STROKES"
170 # Check if they are annotation
171 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
173 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
175 strokes_num
= len(strokes
)
178 strokes_type
= "GP_ANNOTATION"
180 strokes_type
= "NO_STROKES"
182 # Check if they are grease pencil
183 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
185 global global_gpencil_object
186 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
187 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
189 strokes_num
= len(strokes
)
192 strokes_type
= "GP_STROKES"
194 strokes_type
= "NO_STROKES"
196 # Check if they are curves, if there aren't grease pencil strokes
197 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
199 global global_curve_object
200 ob
= bpy
.data
.objects
[global_curve_object
]
201 if ob
.type == "CURVE":
202 strokes_type
= "EXTERNAL_CURVE"
203 strokes_num
= len(ob
.data
.splines
)
205 # Check if there is any non-bezier spline
206 for i
in range(len(ob
.data
.splines
)):
207 if ob
.data
.splines
[i
].type != "BEZIER":
208 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
212 strokes_type
= "EXTERNAL_NO_CURVE"
214 strokes_type
= "NO_STROKES"
216 # Check if they are mesh
218 global global_mesh_object
219 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
220 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
222 # Check if there is a single stroke without any selection in the object
223 if strokes_num
== 1 and total_vert_sel
== 0:
224 if strokes_type
== "EXTERNAL_CURVE":
225 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
226 elif strokes_type
== "GP_STROKES":
227 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
229 if strokes_num
== 0 and total_vert_sel
> 0:
230 strokes_type
= "SELECTION_ALONE"
236 # ----------------------------
237 # Surface generator operator
238 class MESH_OT_SURFSK_add_surface(Operator
):
239 bl_idname
= "mesh.surfsk_add_surface"
240 bl_label
= "Bsurfaces add surface"
241 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
242 bl_options
= {'REGISTER', 'UNDO'}
244 is_crosshatch
: BoolProperty(
247 is_fill_faces
: BoolProperty(
250 selection_U_exists
: BoolProperty(
253 selection_V_exists
: BoolProperty(
256 selection_U2_exists
: BoolProperty(
259 selection_V2_exists
: BoolProperty(
262 selection_V_is_closed
: BoolProperty(
265 selection_U_is_closed
: BoolProperty(
268 selection_V2_is_closed
: BoolProperty(
271 selection_U2_is_closed
: BoolProperty(
275 edges_U
: IntProperty(
277 description
="Number of face-loops crossing the strokes",
282 edges_V
: IntProperty(
284 description
="Number of face-loops following the strokes",
289 cyclic_cross
: BoolProperty(
291 description
="Make cyclic the face-loops crossing the strokes",
294 cyclic_follow
: BoolProperty(
295 name
="Cyclic Follow",
296 description
="Make cyclic the face-loops following the strokes",
299 loops_on_strokes
: BoolProperty(
300 name
="Loops on strokes",
301 description
="Make the loops match the paths of the strokes",
304 automatic_join
: BoolProperty(
305 name
="Automatic join",
306 description
="Join automatically vertices of either surfaces generated "
307 "by crosshatching, or from the borders of closed shapes",
310 join_stretch_factor
: FloatProperty(
312 description
="Amount of stretching or shrinking allowed for "
313 "edges when joining vertices automatically",
319 keep_strokes
: BoolProperty(
321 description
="Keeps the sketched strokes or curves after adding the surface",
324 strokes_type
: StringProperty()
325 initial_global_undo_state
: BoolProperty()
328 def draw(self
, context
):
330 col
= layout
.column(align
=True)
333 if not self
.is_fill_faces
:
335 if not self
.is_crosshatch
:
336 if not self
.selection_U_exists
:
337 col
.prop(self
, "edges_U")
340 if not self
.selection_V_exists
:
341 col
.prop(self
, "edges_V")
346 if not self
.selection_U_exists
:
348 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
349 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
351 col
.prop(self
, "cyclic_cross")
353 if not self
.selection_V_exists
:
355 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
356 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
358 col
.prop(self
, "cyclic_follow")
360 col
.prop(self
, "loops_on_strokes")
362 col
.prop(self
, "automatic_join")
364 if self
.automatic_join
:
368 col
.prop(self
, "join_stretch_factor")
370 col
.prop(self
, "keep_strokes")
372 # Get an ordered list of a chain of vertices
373 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
374 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
375 # Order selected vertices.
377 if closing_vert_idx
is not None:
378 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
380 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
381 prev_v
= first_vert_idx
385 edges_non_matched
= 0
386 for i
in all_selected_edges_idx
:
387 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
388 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
390 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
391 prev_v
= ob
.data
.edges
[i
].vertices
[1]
392 prev_ed
= ob
.data
.edges
[i
]
393 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
394 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
396 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
397 prev_v
= ob
.data
.edges
[i
].vertices
[0]
398 prev_ed
= ob
.data
.edges
[i
]
400 edges_non_matched
+= 1
402 if edges_non_matched
== len(all_selected_edges_idx
):
408 if closing_vert_idx
is not None:
409 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
411 if middle_vertex_idx
is not None:
412 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
413 verts_ordered
.reverse()
415 return tuple(verts_ordered
)
417 # Calculates length of a chain of points.
418 def get_chain_length(self
, object, verts_ordered
):
419 matrix
= object.matrix_world
422 edges_lengths_sum
= 0
423 for i
in range(0, len(verts_ordered
)):
425 prev_v_co
= matrix
@ verts_ordered
[i
].co
427 v_co
= matrix
@ verts_ordered
[i
].co
429 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
430 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
432 edges_lengths
.append(edge_length
)
433 edges_lengths_sum
+= edge_length
437 return edges_lengths
, edges_lengths_sum
439 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
440 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
441 edges_proportions
= []
444 for l
in edges_lengths
:
445 edges_proportions
.append(l
/ edges_lengths_sum
)
449 for _n
in range(0, fixed_edges_num
):
450 edges_proportions
.append(1 / fixed_edges_num
)
453 return edges_proportions
455 # Calculates the angle between two pairs of points in space
456 def orientation_difference(self
, points_A_co
, points_B_co
):
457 # each parameter should be a list with two elements,
458 # and each element should be a x,y,z coordinate
459 vec_A
= points_A_co
[0] - points_A_co
[1]
460 vec_B
= points_B_co
[0] - points_B_co
[1]
462 angle
= vec_A
.angle(vec_B
)
465 angle
= abs(angle
- pi
)
469 # Calculate the which vert of verts_idx list is the nearest one
470 # to the point_co coordinates, and the distance
471 def shortest_distance(self
, object, point_co
, verts_idx
):
472 matrix
= object.matrix_world
474 for i
in range(0, len(verts_idx
)):
475 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
478 nearest_vert_idx
= verts_idx
[i
]
483 nearest_vert_idx
= verts_idx
[i
]
486 return nearest_vert_idx
, shortest_dist
488 # Returns the index of the opposite vert tip in a chain, given a vert tip index
489 # as parameter, and a multidimentional list with all pairs of tips
490 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
491 opposite_vert_tip_idx
= None
492 for i
in range(0, len(all_chains_tips_idx
)):
493 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
494 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
495 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
496 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
498 return opposite_vert_tip_idx
500 # Simplifies a spline and returns the new points coordinates
501 def simplify_spline(self
, spline_coords
, segments_num
):
502 simplified_spline
= []
503 points_between_segments
= round(len(spline_coords
) / segments_num
)
505 simplified_spline
.append(spline_coords
[0])
506 for i
in range(1, segments_num
):
507 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
509 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
511 return simplified_spline
513 # Returns a list with the coords of the points distributed over the splines
514 # passed to this method according to the proportions parameter
515 def distribute_pts(self
, surface_splines
, proportions
):
517 # Calculate the length of each final surface spline
518 surface_splines_lengths
= []
519 surface_splines_parsed
= []
521 for sp_idx
in range(0, len(surface_splines
)):
522 # Calculate spline length
523 surface_splines_lengths
.append(0)
525 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
527 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
529 p
= surface_splines
[sp_idx
].bezier_points
[i
]
530 edge_length
= (prev_p
.co
- p
.co
).length
531 surface_splines_lengths
[sp_idx
] += edge_length
535 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
536 for sp_idx
in range(0, len(surface_splines
)):
537 surface_splines_parsed
.append([])
538 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
540 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
543 for prop_idx
in range(len(proportions
) - 1):
544 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
545 partial_segment_length
= 0
549 # if not it'll pass the p_idx as an index below and crash
550 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
551 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
552 new_dist
= (prev_p_co
- p_co
).length
554 # The new distance that could have the partial segment if
555 # it is still shorter than the target length
556 potential_segment_length
= partial_segment_length
+ new_dist
558 # If the potential is still shorter, keep adding
559 if potential_segment_length
< target_length
:
560 partial_segment_length
= potential_segment_length
565 # If the potential is longer than the target, calculate the target
566 # (a point between the last two points), and assign
567 elif potential_segment_length
> target_length
:
568 remaining_dist
= target_length
- partial_segment_length
569 vec
= p_co
- prev_p_co
571 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
573 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
575 partial_segment_length
+= remaining_dist
576 prev_p_co
= intermediate_co
580 # If the potential is equal to the target, assign
581 elif potential_segment_length
== target_length
:
582 surface_splines_parsed
[sp_idx
].append(p_co
)
590 # last point of the spline
591 surface_splines_parsed
[sp_idx
].append(
592 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
595 return surface_splines_parsed
597 # Counts the number of faces that belong to each edge
598 def edge_face_count(self
, ob
):
599 ed_keys_count_dict
= {}
601 for face
in ob
.data
.polygons
:
602 for ed_keys
in face
.edge_keys
:
603 if ed_keys
not in ed_keys_count_dict
:
604 ed_keys_count_dict
[ed_keys
] = 1
606 ed_keys_count_dict
[ed_keys
] += 1
609 for i
in range(len(ob
.data
.edges
)):
610 edge_face_count
.append(0)
612 for i
in range(len(ob
.data
.edges
)):
613 ed
= ob
.data
.edges
[i
]
618 if (v1
, v2
) in ed_keys_count_dict
:
619 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
620 elif (v2
, v1
) in ed_keys_count_dict
:
621 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
623 return edge_face_count
625 # Fills with faces all the selected vertices which form empty triangles or quads
626 def fill_with_faces(self
, object):
627 all_selected_verts_count
= self
.main_object_selected_verts_count
629 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
631 # Calculate average length of selected edges
632 all_selected_verts
= []
633 original_sel_edges_count
= 0
634 for ed
in object.data
.edges
:
635 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
637 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
638 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
640 original_sel_edges_count
+= 1
642 if not ed
.vertices
[0] in all_selected_verts
:
643 all_selected_verts
.append(ed
.vertices
[0])
645 if not ed
.vertices
[1] in all_selected_verts
:
646 all_selected_verts
.append(ed
.vertices
[1])
648 tuple(all_selected_verts
)
650 # Check if there is any edge selected. If not, interrupt the script
651 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
654 # Get all edges connected to selected verts
655 all_edges_around_sel_verts
= []
656 edges_connected_to_sel_verts
= {}
657 verts_connected_to_every_vert
= {}
658 for ed_idx
in range(len(object.data
.edges
)):
659 ed
= object.data
.edges
[ed_idx
]
662 if ed
.vertices
[0] in all_selected_verts
:
663 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
664 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
666 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
669 if ed
.vertices
[1] in all_selected_verts
:
670 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
671 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
673 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
676 if include_edge
is True:
677 all_edges_around_sel_verts
.append(ed_idx
)
679 # Get all connected verts to each vert
680 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
681 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
683 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
684 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
686 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
687 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
689 # Get all verts connected to faces
690 all_verts_part_of_faces
= []
691 all_edges_faces_count
= []
692 all_edges_faces_count
+= self
.edge_face_count(object)
694 # Get only the selected edges that have faces attached.
695 count_faces_of_edges_around_sel_verts
= {}
696 selected_verts_with_faces
= []
697 for ed_idx
in all_edges_around_sel_verts
:
698 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
700 if all_edges_faces_count
[ed_idx
] > 0:
701 ed
= object.data
.edges
[ed_idx
]
703 if not ed
.vertices
[0] in selected_verts_with_faces
:
704 selected_verts_with_faces
.append(ed
.vertices
[0])
706 if not ed
.vertices
[1] in selected_verts_with_faces
:
707 selected_verts_with_faces
.append(ed
.vertices
[1])
709 all_verts_part_of_faces
.append(ed
.vertices
[0])
710 all_verts_part_of_faces
.append(ed
.vertices
[1])
712 tuple(selected_verts_with_faces
)
714 # Discard unneeded verts from calculations
715 participating_verts
= []
717 for v_idx
in all_selected_verts
:
718 vert_has_edges_with_one_face
= False
720 # Check if the actual vert has at least one edge connected to only one face
721 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
722 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
723 vert_has_edges_with_one_face
= True
725 # If the vert has two or less edges connected and the vert is not part of any face.
726 # Or the vert is part of any face and at least one of
727 # the connected edges has only one face attached to it.
728 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
729 v_idx
not in all_verts_part_of_faces
) or \
730 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
731 (v_idx
in all_verts_part_of_faces
and
732 vert_has_edges_with_one_face
):
734 participating_verts
.append(v_idx
)
736 if v_idx
not in all_verts_part_of_faces
:
737 movable_verts
.append(v_idx
)
739 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
740 for mv_idx
in movable_verts
:
742 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
744 for actual_v_idx
in all_selected_verts
:
745 count_shared_neighbors
= 0
748 for mv_conn_v_idx
in mv_connected_verts
:
749 if mv_idx
!= actual_v_idx
:
750 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
751 mv_conn_v_idx
not in checked_verts
:
752 count_shared_neighbors
+= 1
753 checked_verts
.append(mv_conn_v_idx
)
755 if actual_v_idx
in mv_connected_verts
:
759 if count_shared_neighbors
== 2:
767 movable_verts
.remove(mv_idx
)
769 # Calculate merge distance for participating verts
770 shortest_edge_length
= None
771 for ed
in object.data
.edges
:
772 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
773 v1
= object.data
.vertices
[ed
.vertices
[0]]
774 v2
= object.data
.vertices
[ed
.vertices
[1]]
776 length
= (v1
.co
- v2
.co
).length
778 if shortest_edge_length
is None:
779 shortest_edge_length
= length
781 if length
< shortest_edge_length
:
782 shortest_edge_length
= length
784 if shortest_edge_length
is not None:
785 edges_merge_distance
= shortest_edge_length
* 0.5
787 edges_merge_distance
= 0
789 # Get together the verts near enough. They will be merged later
791 remaining_verts
+= participating_verts
792 for v1_idx
in participating_verts
:
793 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
795 coords_verts_to_merge
= {}
797 verts_to_merge
.append(v1_idx
)
799 v1_co
= object.data
.vertices
[v1_idx
].co
800 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
802 for v2_idx
in remaining_verts
:
804 v2_co
= object.data
.vertices
[v2_idx
].co
806 dist
= (v1_co
- v2_co
).length
808 if dist
<= edges_merge_distance
: # Add the verts which are near enough
809 verts_to_merge
.append(v2_idx
)
811 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
813 for vm_idx
in verts_to_merge
:
814 remaining_verts
.remove(vm_idx
)
816 if len(verts_to_merge
) > 1:
817 # Calculate middle point of the verts to merge.
821 movable_verts_to_merge_count
= 0
822 for i
in range(len(verts_to_merge
)):
823 if verts_to_merge
[i
] in movable_verts
:
824 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
830 movable_verts_to_merge_count
+= 1
833 sum_x_co
/ movable_verts_to_merge_count
,
834 sum_y_co
/ movable_verts_to_merge_count
,
835 sum_z_co
/ movable_verts_to_merge_count
838 # Check if any vert to be merged is not movable
840 are_verts_not_movable
= False
841 verts_not_movable
= []
842 for v_merge_idx
in verts_to_merge
:
843 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
844 are_verts_not_movable
= True
845 verts_not_movable
.append(v_merge_idx
)
847 if are_verts_not_movable
:
848 # Get the vert connected to faces, that is nearest to
849 # the middle point of the movable verts
851 for vcf_idx
in verts_not_movable
:
852 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
853 Vector(middle_point_co
)).length
)
855 if shortest_dist
is None:
857 nearest_vert_idx
= vcf_idx
859 if dist
< shortest_dist
:
861 nearest_vert_idx
= vcf_idx
863 coords
= object.data
.vertices
[nearest_vert_idx
].co
864 target_point_co
= [coords
[0], coords
[1], coords
[2]]
866 target_point_co
= middle_point_co
868 # Move verts to merge to the middle position
869 for v_merge_idx
in verts_to_merge
:
870 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
871 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
872 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
873 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
875 # Perform "Remove Doubles" to weld all the disconnected verts
876 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
877 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
879 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
881 # Get all the definitive selected edges, after weldding
883 edges_per_vert
= {} # Number of faces of each selected edge
884 for ed
in object.data
.edges
:
885 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
886 selected_edges
.append(ed
.index
)
888 # Save all the edges that belong to each vertex.
889 if not ed
.vertices
[0] in edges_per_vert
:
890 edges_per_vert
[ed
.vertices
[0]] = []
892 if not ed
.vertices
[1] in edges_per_vert
:
893 edges_per_vert
[ed
.vertices
[1]] = []
895 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
896 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
898 # Check if all the edges connected to each vert have two faces attached to them.
899 # To discard them later and make calculations faster
901 a
+= self
.edge_face_count(object)
903 verts_surrounded_by_faces
= {}
904 for v_idx
in edges_per_vert
:
905 edges_with_two_faces_count
= 0
907 for ed_idx
in edges_per_vert
[v_idx
]:
909 edges_with_two_faces_count
+= 1
911 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
912 verts_surrounded_by_faces
[v_idx
] = True
914 verts_surrounded_by_faces
[v_idx
] = False
916 # Get all the selected vertices
917 selected_verts_idx
= []
918 for v
in object.data
.vertices
:
920 selected_verts_idx
.append(v
.index
)
922 # Get all the faces of the object
923 all_object_faces_verts_idx
= []
924 for face
in object.data
.polygons
:
926 face_verts
.append(face
.vertices
[0])
927 face_verts
.append(face
.vertices
[1])
928 face_verts
.append(face
.vertices
[2])
930 if len(face
.vertices
) == 4:
931 face_verts
.append(face
.vertices
[3])
933 all_object_faces_verts_idx
.append(face_verts
)
935 # Deselect all vertices
936 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
937 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
938 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
940 # Make a dictionary with the verts related to each vert
941 related_key_verts
= {}
942 for ed_idx
in selected_edges
:
943 ed
= object.data
.edges
[ed_idx
]
945 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
946 if not ed
.vertices
[0] in related_key_verts
:
947 related_key_verts
[ed
.vertices
[0]] = []
949 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
950 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
952 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
953 if not ed
.vertices
[1] in related_key_verts
:
954 related_key_verts
[ed
.vertices
[1]] = []
956 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
957 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
959 # Get groups of verts forming each face
961 for v1
in related_key_verts
: # verts-1 ....
962 for v2
in related_key_verts
: # verts-2
964 related_verts_in_common
= []
967 for rel_v1
in related_key_verts
[v1
]:
968 # Check if related verts of verts-1 are related verts of verts-2
969 if rel_v1
in related_key_verts
[v2
]:
970 related_verts_in_common
.append(rel_v1
)
972 if v2
in related_key_verts
[v1
]:
975 if v1
in related_key_verts
[v2
]:
978 repeated_face
= False
979 # If two verts have two related verts in common, they form a quad
980 if len(related_verts_in_common
) == 2:
981 # Check if the face is already saved
982 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
984 for f_verts
in all_faces_to_check_idx
:
987 if len(f_verts
) == 4:
992 if related_verts_in_common
[0] in f_verts
:
994 if related_verts_in_common
[1] in f_verts
:
997 if repeated_verts
== len(f_verts
):
1001 if not repeated_face
:
1002 faces_verts_idx
.append(
1003 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
1006 # If Two verts have one related vert in common and
1007 # they are related to each other, they form a triangle
1008 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1009 # Check if the face is already saved.
1010 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1012 for f_verts
in all_faces_to_check_idx
:
1015 if len(f_verts
) == 3:
1020 if related_verts_in_common
[0] in f_verts
:
1023 if repeated_verts
== len(f_verts
):
1024 repeated_face
= True
1027 if not repeated_face
:
1028 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1030 # Keep only the faces that don't overlap by ignoring quads
1031 # that overlap with two adjacent triangles
1032 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1033 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1034 for i
in range(len(faces_verts_idx
)):
1035 for t
in range(len(all_faces_to_check_idx
)):
1039 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1040 for v_idx
in all_faces_to_check_idx
[t
]:
1041 if v_idx
in faces_verts_idx
[i
]:
1042 verts_in_common
+= 1
1043 # If it doesn't have all it's vertices repeated in the other face
1044 if verts_in_common
== 3:
1045 if i
not in faces_to_not_include_idx
:
1046 faces_to_not_include_idx
.append(i
)
1048 # Build faces discarding the ones in faces_to_not_include
1053 num_faces_created
= 0
1054 for i
in range(len(faces_verts_idx
)):
1055 if i
not in faces_to_not_include_idx
:
1056 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1058 num_faces_created
+= 1
1063 for v_idx
in selected_verts_idx
:
1064 self
.main_object
.data
.vertices
[v_idx
].select
= True
1066 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1067 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1068 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1072 return num_faces_created
1074 # Crosshatch skinning
1075 def crosshatch_surface_invoke(self
, ob_original_splines
):
1076 self
.is_crosshatch
= False
1077 self
.crosshatch_merge_distance
= 0
1079 objects_to_delete
= [] # duplicated strokes to be deleted.
1081 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1082 # (without this the surface verts merging with the main object doesn't work well)
1083 self
.modifiers_prev_viewport_state
= []
1084 if len(self
.main_object
.modifiers
) > 0:
1085 for m_idx
in range(len(self
.main_object
.modifiers
)):
1086 self
.modifiers_prev_viewport_state
.append(
1087 self
.main_object
.modifiers
[m_idx
].show_viewport
1089 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1091 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1092 ob_original_splines
.select_set(True)
1093 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1095 if len(ob_original_splines
.data
.splines
) >= 2:
1096 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1097 ob_splines
= bpy
.context
.object
1098 ob_splines
.name
= "SURFSKIO_NE_STR"
1100 # Get estimative merge distance (sum up the distances from the first point to
1101 # all other points, then average them and then divide them)
1102 first_point_dist_sum
= 0
1105 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1106 for i
in range(len(ob_splines
.data
.splines
)):
1107 sp
= ob_splines
.data
.splines
[i
]
1109 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1110 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1112 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1113 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1115 first_point_dist_sum
+= first_dist
+ second_dist
1119 shortest_dist
= first_dist
1120 elif second_dist
!= 0:
1121 shortest_dist
= second_dist
1123 if shortest_dist
> first_dist
and first_dist
!= 0:
1124 shortest_dist
= first_dist
1126 if shortest_dist
> second_dist
and second_dist
!= 0:
1127 shortest_dist
= second_dist
1129 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1131 # Recalculation of merge distance
1133 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1135 ob_calc_merge_dist
= bpy
.context
.object
1136 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1138 objects_to_delete
.append(ob_calc_merge_dist
)
1140 # Smooth out strokes a little to improve crosshatch detection
1141 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1142 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1145 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1147 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1148 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1150 # Convert curves into mesh
1151 ob_calc_merge_dist
.data
.resolution_u
= 12
1152 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1154 # Find "intersection-nodes"
1155 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1156 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1157 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1158 threshold
=self
.crosshatch_merge_distance
)
1159 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1160 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1162 # Remove verts with less than three edges
1163 verts_edges_count
= {}
1164 for ed
in ob_calc_merge_dist
.data
.edges
:
1167 if v
[0] not in verts_edges_count
:
1168 verts_edges_count
[v
[0]] = 0
1170 if v
[1] not in verts_edges_count
:
1171 verts_edges_count
[v
[1]] = 0
1173 verts_edges_count
[v
[0]] += 1
1174 verts_edges_count
[v
[1]] += 1
1176 nodes_verts_coords
= []
1177 for v_idx
in verts_edges_count
:
1178 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1180 if verts_edges_count
[v_idx
] < 3:
1184 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1185 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1186 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1188 # Remove doubles to discard very near verts from calculations of distance
1189 bpy
.ops
.mesh
.remove_doubles(
1190 'INVOKE_REGION_WIN',
1191 threshold
=self
.crosshatch_merge_distance
* 4.0
1193 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1195 # Get all coords of the resulting nodes
1196 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1197 v
in ob_calc_merge_dist
.data
.vertices
]
1199 # Check if the strokes are a crosshatch
1200 if len(nodes_verts_coords
) >= 3:
1201 self
.is_crosshatch
= True
1203 shortest_dist
= None
1204 for co_1
in nodes_verts_coords
:
1205 for co_2
in nodes_verts_coords
:
1207 dist
= (Vector(co_1
) - Vector(co_2
)).length
1209 if shortest_dist
is not None:
1210 if dist
< shortest_dist
:
1211 shortest_dist
= dist
1213 shortest_dist
= dist
1215 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1217 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1218 ob_splines
.select_set(True)
1219 bpy
.context
.view_layer
.objects
.active
= ob_splines
1221 # Deselect all points
1222 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1223 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1224 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1226 # Smooth splines in a localized way, to eliminate "saw-teeth"
1227 # like shapes when there are many points
1228 for sp
in ob_splines
.data
.splines
:
1231 angle_limit
= 2 # Degrees
1232 for t
in range(len(sp
.bezier_points
)):
1233 # Because on each iteration it checks the "next two points"
1234 # of the actual. This way it doesn't go out of range
1235 if t
<= len(sp
.bezier_points
) - 3:
1236 p1
= sp
.bezier_points
[t
]
1237 p2
= sp
.bezier_points
[t
+ 1]
1238 p3
= sp
.bezier_points
[t
+ 2]
1240 vec_1
= p1
.co
- p2
.co
1241 vec_2
= p2
.co
- p3
.co
1243 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1244 angle
= vec_1
.angle(vec_2
)
1245 angle_sum
+= degrees(angle
)
1247 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1248 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1249 p1
.select_control_point
= True
1250 p1
.select_left_handle
= True
1251 p1
.select_right_handle
= True
1253 p2
.select_control_point
= True
1254 p2
.select_left_handle
= True
1255 p2
.select_right_handle
= True
1257 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1258 p3
.select_control_point
= True
1259 p3
.select_left_handle
= True
1260 p3
.select_right_handle
= True
1264 sp
.bezier_points
[0].select_control_point
= False
1265 sp
.bezier_points
[0].select_left_handle
= False
1266 sp
.bezier_points
[0].select_right_handle
= False
1268 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1269 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1270 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1272 # Smooth out strokes a little to improve crosshatch detection
1273 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1276 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1278 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1279 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1281 # Simplify the splines
1282 for sp
in ob_splines
.data
.splines
:
1285 sp
.bezier_points
[0].select_control_point
= True
1286 sp
.bezier_points
[0].select_left_handle
= True
1287 sp
.bezier_points
[0].select_right_handle
= True
1289 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1290 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1291 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1293 angle_limit
= 15 # Degrees
1294 for t
in range(len(sp
.bezier_points
)):
1295 # Because on each iteration it checks the "next two points"
1296 # of the actual. This way it doesn't go out of range
1297 if t
<= len(sp
.bezier_points
) - 3:
1298 p1
= sp
.bezier_points
[t
]
1299 p2
= sp
.bezier_points
[t
+ 1]
1300 p3
= sp
.bezier_points
[t
+ 2]
1302 vec_1
= p1
.co
- p2
.co
1303 vec_2
= p2
.co
- p3
.co
1305 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1306 angle
= vec_1
.angle(vec_2
)
1307 angle_sum
+= degrees(angle
)
1308 # If sum of angles is grater than the limit
1309 if angle_sum
>= angle_limit
:
1310 p1
.select_control_point
= True
1311 p1
.select_left_handle
= True
1312 p1
.select_right_handle
= True
1314 p2
.select_control_point
= True
1315 p2
.select_left_handle
= True
1316 p2
.select_right_handle
= True
1318 p3
.select_control_point
= True
1319 p3
.select_left_handle
= True
1320 p3
.select_right_handle
= True
1324 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1325 bpy
.ops
.curve
.select_all(action
='INVERT')
1327 bpy
.ops
.curve
.delete(type='VERT')
1328 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1330 objects_to_delete
.append(ob_splines
)
1332 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1333 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1334 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1336 # Check if the strokes are a crosshatch
1337 if self
.is_crosshatch
:
1338 all_points_coords
= []
1339 for i
in range(len(ob_splines
.data
.splines
)):
1340 all_points_coords
.append([])
1342 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1343 x
, y
, z
in [bp
.co
for
1344 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1346 all_intersections
= []
1347 checked_splines
= []
1348 for i
in range(len(all_points_coords
)):
1350 for t
in range(len(all_points_coords
[i
]) - 1):
1351 bp1_co
= all_points_coords
[i
][t
]
1352 bp2_co
= all_points_coords
[i
][t
+ 1]
1354 for i2
in range(len(all_points_coords
)):
1355 if i
!= i2
and i2
not in checked_splines
:
1356 for t2
in range(len(all_points_coords
[i2
]) - 1):
1357 bp3_co
= all_points_coords
[i2
][t2
]
1358 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1360 intersec_coords
= intersect_line_line(
1361 bp1_co
, bp2_co
, bp3_co
, bp4_co
1363 if intersec_coords
is not None:
1364 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1366 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1367 _temp_co
, percent1
= intersect_point_line(
1368 intersec_coords
[0], bp1_co
, bp2_co
1370 if (percent1
>= -0.02 and percent1
<= 1.02):
1371 _temp_co
, percent2
= intersect_point_line(
1372 intersec_coords
[1], bp3_co
, bp4_co
1374 if (percent2
>= -0.02 and percent2
<= 1.02):
1375 # Format: spline index, first point index from
1376 # corresponding segment, percentage from first point of
1377 # actual segment, coords of intersection point
1378 all_intersections
.append(
1380 ob_splines
.matrix_world
@ intersec_coords
[0])
1382 all_intersections
.append(
1384 ob_splines
.matrix_world
@ intersec_coords
[1])
1387 checked_splines
.append(i
)
1388 # Sort list by spline, then by corresponding first point index of segment,
1389 # and then by percentage from first point of segment: elements 0 and 1 respectively
1390 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1392 self
.crosshatch_strokes_coords
= {}
1393 for i
in range(len(all_intersections
)):
1394 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1395 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1397 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1398 all_intersections
[i
][3]
1399 ) # Save intersection coords
1401 self
.is_crosshatch
= False
1403 # Delete all duplicates
1404 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1406 # If the main object has modifiers, turn their "viewport view status" to
1407 # what it was before the forced deactivation above
1408 if len(self
.main_object
.modifiers
) > 0:
1409 for m_idx
in range(len(self
.main_object
.modifiers
)):
1410 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1416 # Part of the Crosshatch process that is repeated when the operator is tweaked
1417 def crosshatch_surface_execute(self
, context
):
1418 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1419 # (without this the surface verts merging with the main object doesn't work well)
1420 self
.modifiers_prev_viewport_state
= []
1421 if len(self
.main_object
.modifiers
) > 0:
1422 for m_idx
in range(len(self
.main_object
.modifiers
)):
1423 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1425 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1427 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1429 me_name
= "SURFSKIO_STK_TMP"
1430 me
= bpy
.data
.meshes
.new(me_name
)
1432 all_verts_coords
= []
1434 for st_idx
in self
.crosshatch_strokes_coords
:
1435 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1436 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1438 all_verts_coords
.append(coords
)
1441 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1443 me
.from_pydata(all_verts_coords
, all_edges
, [])
1444 ob
= object_utils
.object_data_add(context
, me
)
1445 ob
.location
= (0.0, 0.0, 0.0)
1446 ob
.rotation_euler
= (0.0, 0.0, 0.0)
1447 ob
.scale
= (1.0, 1.0, 1.0)
1449 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1451 bpy
.context
.view_layer
.objects
.active
= ob
1453 # Get together each vert and its nearest, to the middle position
1454 verts
= ob
.data
.vertices
1456 for i
in range(len(verts
)):
1457 shortest_dist
= None
1459 if i
not in checked_verts
:
1460 for t
in range(len(verts
)):
1461 if i
!= t
and t
not in checked_verts
:
1462 dist
= (verts
[i
].co
- verts
[t
].co
).length
1464 if shortest_dist
is not None:
1465 if dist
< shortest_dist
:
1466 shortest_dist
= dist
1469 shortest_dist
= dist
1472 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1474 verts
[i
].co
= middle_location
1475 verts
[nearest_vert
].co
= middle_location
1477 checked_verts
.append(i
)
1478 checked_verts
.append(nearest_vert
)
1480 # Calculate average length between all the generated edges
1481 ob
= bpy
.context
.object
1483 for ed
in ob
.data
.edges
:
1484 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1485 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1487 lengths_sum
+= (v1
.co
- v2
.co
).length
1489 edges_count
= len(ob
.data
.edges
)
1490 # possible division by zero here
1491 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1493 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1494 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1495 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1496 threshold
=average_edge_length
/ 15.0)
1497 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1499 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1501 # Make a dictionary with the verts related to each vert
1502 related_key_verts
= {}
1503 for ed
in final_points_ob
.data
.edges
:
1504 if not ed
.vertices
[0] in related_key_verts
:
1505 related_key_verts
[ed
.vertices
[0]] = []
1507 if not ed
.vertices
[1] in related_key_verts
:
1508 related_key_verts
[ed
.vertices
[1]] = []
1510 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1511 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1513 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1514 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1516 # Get groups of verts forming each face
1517 faces_verts_idx
= []
1518 for v1
in related_key_verts
: # verts-1 ....
1519 for v2
in related_key_verts
: # verts-2
1521 related_verts_in_common
= []
1522 v2_in_rel_v1
= False
1523 v1_in_rel_v2
= False
1524 for rel_v1
in related_key_verts
[v1
]:
1525 # Check if related verts of verts-1 are related verts of verts-2
1526 if rel_v1
in related_key_verts
[v2
]:
1527 related_verts_in_common
.append(rel_v1
)
1529 if v2
in related_key_verts
[v1
]:
1532 if v1
in related_key_verts
[v2
]:
1535 repeated_face
= False
1536 # If two verts have two related verts in common, they form a quad
1537 if len(related_verts_in_common
) == 2:
1538 # Check if the face is already saved
1539 for f_verts
in faces_verts_idx
:
1542 if len(f_verts
) == 4:
1547 if related_verts_in_common
[0] in f_verts
:
1549 if related_verts_in_common
[1] in f_verts
:
1552 if repeated_verts
== len(f_verts
):
1553 repeated_face
= True
1556 if not repeated_face
:
1557 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1558 v2
, related_verts_in_common
[1]])
1560 # If Two verts have one related vert in common and they are
1561 # related to each other, they form a triangle
1562 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1563 # Check if the face is already saved.
1564 for f_verts
in faces_verts_idx
:
1567 if len(f_verts
) == 3:
1572 if related_verts_in_common
[0] in f_verts
:
1575 if repeated_verts
== len(f_verts
):
1576 repeated_face
= True
1579 if not repeated_face
:
1580 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1582 # Keep only the faces that don't overlap by ignoring
1583 # quads that overlap with two adjacent triangles
1584 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1585 for i
in range(len(faces_verts_idx
)):
1586 for t
in range(len(faces_verts_idx
)):
1590 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1591 for v_idx
in faces_verts_idx
[t
]:
1592 if v_idx
in faces_verts_idx
[i
]:
1593 verts_in_common
+= 1
1594 # If it doesn't have all it's vertices repeated in the other face
1595 if verts_in_common
== 3:
1596 if i
not in faces_to_not_include_idx
:
1597 faces_to_not_include_idx
.append(i
)
1600 all_surface_verts_co
= []
1601 for i
in range(len(final_points_ob
.data
.vertices
)):
1602 coords
= final_points_ob
.data
.vertices
[i
].co
1603 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1605 # Verts of each face.
1606 all_surface_faces
= []
1607 for i
in range(len(faces_verts_idx
)):
1608 if i
not in faces_to_not_include_idx
:
1610 for v_idx
in faces_verts_idx
[i
]:
1613 all_surface_faces
.append(face
)
1616 surf_me_name
= "SURFSKIO_surface"
1617 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1618 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1619 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1620 ob_surface
.location
= (0.0, 0.0, 0.0)
1621 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
1622 ob_surface
.scale
= (1.0, 1.0, 1.0)
1624 # Delete final points temporal object
1625 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1627 # Delete isolated verts if there are any
1628 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1629 ob_surface
.select_set(True)
1630 bpy
.context
.view_layer
.objects
.active
= ob_surface
1632 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1633 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1634 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1635 bpy
.ops
.mesh
.delete()
1636 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1638 # Join crosshatch results with original mesh
1640 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1641 edges_length_sum
= 0
1642 for ed
in ob_surface
.data
.edges
:
1643 edges_length_sum
+= (
1644 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1645 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1648 # Make dictionary with all the verts connected to each vert, on the new surface object.
1649 surface_connected_verts
= {}
1650 for ed
in ob_surface
.data
.edges
:
1651 if not ed
.vertices
[0] in surface_connected_verts
:
1652 surface_connected_verts
[ed
.vertices
[0]] = []
1654 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1656 if ed
.vertices
[1] not in surface_connected_verts
:
1657 surface_connected_verts
[ed
.vertices
[1]] = []
1659 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1661 # Duplicate the new surface object, and use shrinkwrap to
1662 # calculate later the nearest verts to the main object
1663 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1664 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1665 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1667 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1669 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1671 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1672 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1673 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1674 shrinkwrap_modifier
.target
= self
.main_object
1676 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap_modifier
.name
)
1678 # Make list with verts of original mesh as index and coords as value
1679 main_object_verts_coords
= []
1680 for v
in self
.main_object
.data
.vertices
:
1681 coords
= self
.main_object
.matrix_world
@ v
.co
1683 # To avoid problems when taking "-0.00" as a different value as "0.00"
1684 for c
in range(len(coords
)):
1685 if "%.3f" % coords
[c
] == "-0.00":
1688 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1690 tuple(main_object_verts_coords
)
1692 # Determine which verts will be merged, snap them to the nearest verts
1693 # on the original verts, and get them selected
1694 crosshatch_verts_to_merge
= []
1695 if self
.automatic_join
:
1696 for i
in range(len(ob_surface
.data
.vertices
)-1):
1697 # Calculate the distance from each of the connected verts to the actual vert,
1698 # and compare it with the distance they would have if joined.
1699 # If they don't change much, that vert can be joined
1700 merge_actual_vert
= True
1702 if len(surface_connected_verts
[i
]) < 4:
1703 for c_v_idx
in surface_connected_verts
[i
]:
1704 points_original
= []
1705 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1706 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1709 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1710 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1712 vec_A
= points_original
[0] - points_original
[1]
1713 vec_B
= points_target
[0] - points_target
[1]
1715 dist_A
= (points_original
[0] - points_original
[1]).length
1716 dist_B
= (points_target
[0] - points_target
[1]).length
1719 points_original
[0] == points_original
[1] or
1720 points_target
[0] == points_target
[1]
1721 ): # If any vector's length is zero
1723 angle
= vec_A
.angle(vec_B
) / pi
1727 # Set a range of acceptable variation in the connected edges
1728 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1729 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1730 angle
>= 0.15 * self
.join_stretch_factor
:
1732 merge_actual_vert
= False
1735 merge_actual_vert
= False
1737 self
.report({'WARNING'},
1738 "Crosshatch set incorrectly")
1740 if merge_actual_vert
:
1741 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1742 # To avoid problems when taking "-0.000" as a different value as "0.00"
1743 for c
in range(len(coords
)):
1744 if "%.3f" % coords
[c
] == "-0.00":
1747 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1749 if comparison_coords
in main_object_verts_coords
:
1750 # Get the index of the vert with those coords in the main object
1751 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1753 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1754 self
.main_object_selected_verts_count
== 0:
1756 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1757 ob_surface
.data
.vertices
[i
].select
= True
1758 crosshatch_verts_to_merge
.append(i
)
1760 # Make sure the vert in the main object is selected,
1761 # in case it wasn't selected and the "join crosshatch" option is active
1762 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1764 # Delete duplicated object
1765 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1767 # Join crosshatched surface and main object
1768 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1769 ob_surface
.select_set(True)
1770 self
.main_object
.select_set(True)
1771 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1773 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1775 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1776 # Perform Remove doubles to merge verts
1777 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1778 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1780 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1782 # If the main object has modifiers, turn their "viewport view status"
1783 # to what it was before the forced deactivation above
1784 if len(self
.main_object
.modifiers
) > 0:
1785 for m_idx
in range(len(self
.main_object
.modifiers
)):
1786 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1792 def rectangular_surface(self
, context
):
1794 all_selected_edges_idx
= []
1795 all_selected_verts
= []
1797 for ed
in self
.main_object
.data
.edges
:
1799 all_selected_edges_idx
.append(ed
.index
)
1802 if not ed
.vertices
[0] in all_selected_verts
:
1803 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1804 if not ed
.vertices
[1] in all_selected_verts
:
1805 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1807 # All verts (both from each edge) to determine later
1808 # which are at the tips (those not repeated twice)
1809 all_verts_idx
.append(ed
.vertices
[0])
1810 all_verts_idx
.append(ed
.vertices
[1])
1812 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1813 all_chains_tips_idx
= []
1814 for v_idx
in all_verts_idx
:
1815 if all_verts_idx
.count(v_idx
) < 2:
1816 all_chains_tips_idx
.append(v_idx
)
1818 edges_connected_to_tips
= []
1819 for ed
in self
.main_object
.data
.edges
:
1820 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1821 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1823 edges_connected_to_tips
.append(ed
)
1825 # Check closed selections
1826 # List with groups of three verts, where the first element of the pair is
1827 # the unselected vert of a closed selection and the other two elements are the
1828 # selected neighbor verts (it will be useful to determine which selection chain
1829 # the unselected vert belongs to, and determine the "middle-vertex")
1830 single_unselected_verts_and_neighbors
= []
1832 # To identify a "closed" selection (a selection that is a closed chain except
1833 # for one vertex) find the vertex in common that have the edges connected to tips.
1834 # If there is a vertex in common, that one is the unselected vert that closes
1835 # the selection or is a "middle-vertex"
1836 single_unselected_verts
= []
1837 for ed
in edges_connected_to_tips
:
1838 for ed_b
in edges_connected_to_tips
:
1840 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1841 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1842 ed
.vertices
[0] not in single_unselected_verts
:
1844 # The second element is one of the tips of the selected
1845 # vertices of the closed selection
1846 single_unselected_verts_and_neighbors
.append(
1847 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1849 single_unselected_verts
.append(ed
.vertices
[0])
1851 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1852 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1853 ed
.vertices
[0] not in single_unselected_verts
:
1855 single_unselected_verts_and_neighbors
.append(
1856 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1858 single_unselected_verts
.append(ed
.vertices
[0])
1860 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1861 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1862 ed
.vertices
[1] not in single_unselected_verts
:
1864 single_unselected_verts_and_neighbors
.append(
1865 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1867 single_unselected_verts
.append(ed
.vertices
[1])
1869 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1870 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1871 ed
.vertices
[1] not in single_unselected_verts
:
1873 single_unselected_verts_and_neighbors
.append(
1874 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1876 single_unselected_verts
.append(ed
.vertices
[1])
1879 middle_vertex_idx
= None
1880 tips_to_discard_idx
= []
1882 # Check if there is a "middle-vertex", and get its index
1883 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1884 actual_chain_verts
= self
.get_ordered_verts(
1885 self
.main_object
, all_selected_edges_idx
,
1886 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1890 if single_unselected_verts_and_neighbors
[i
][2] != \
1891 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1893 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1894 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1895 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1897 # List with pairs of verts that belong to the tips of each selection chain (row)
1898 verts_tips_same_chain_idx
= []
1899 if len(all_chains_tips_idx
) >= 2:
1901 for i
in range(0, len(all_chains_tips_idx
)):
1902 if all_chains_tips_idx
[i
] not in checked_v
:
1903 v_chain
= self
.get_ordered_verts(
1904 self
.main_object
, all_selected_edges_idx
,
1905 all_verts_idx
, all_chains_tips_idx
[i
],
1906 middle_vertex_idx
, None
1909 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1911 checked_v
.append(v_chain
[0].index
)
1912 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1914 # Selection tips (vertices).
1915 verts_tips_parsed_idx
= []
1916 if len(all_chains_tips_idx
) >= 2:
1917 for spec_v_idx
in all_chains_tips_idx
:
1918 if (spec_v_idx
not in tips_to_discard_idx
):
1919 verts_tips_parsed_idx
.append(spec_v_idx
)
1921 # Identify the type of selection made by the user
1922 if middle_vertex_idx
is not None:
1923 # If there are 4 tips (two selection chains), and
1924 # there is only one single unselected vert (the middle vert)
1925 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1926 selection_type
= "TWO_CONNECTED"
1928 # The type of the selection was not identified, the script stops.
1929 self
.report({'WARNING'}, "The selection isn't valid.")
1931 self
.stopping_errors
= True
1935 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1936 selection_type
= "SINGLE"
1937 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1938 selection_type
= "TWO_NOT_CONNECTED"
1939 elif len(all_chains_tips_idx
) == 0:
1940 if len(self
.main_splines
.data
.splines
) > 1:
1941 selection_type
= "NO_SELECTION"
1943 # If the selection was not identified and there is only one stroke,
1944 # there's no possibility to build a surface, so the script is interrupted
1945 self
.report({'WARNING'}, "The selection isn't valid.")
1947 self
.stopping_errors
= True
1951 # The type of the selection was not identified, the script stops
1952 self
.report({'WARNING'}, "The selection isn't valid.")
1954 self
.stopping_errors
= True
1958 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1959 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1960 self
.report({'WARNING'},
1961 "At least two strokes are needed when there are two not connected selections")
1963 self
.stopping_errors
= True
1967 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1969 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1970 self
.main_splines
.select_set(True)
1971 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1973 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1974 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1975 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1976 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1977 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1978 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1979 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1980 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1981 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1983 self
.selection_U_exists
= False
1984 self
.selection_U2_exists
= False
1985 self
.selection_V_exists
= False
1986 self
.selection_V2_exists
= False
1988 self
.selection_U_is_closed
= False
1989 self
.selection_U2_is_closed
= False
1990 self
.selection_V_is_closed
= False
1991 self
.selection_V2_is_closed
= False
1993 # Define what vertices are at the tips of each selection and are not the middle-vertex
1994 if selection_type
== "TWO_CONNECTED":
1995 self
.selection_U_exists
= True
1996 self
.selection_V_exists
= True
1998 closing_vert_U_idx
= None
1999 closing_vert_V_idx
= None
2000 closing_vert_U2_idx
= None
2001 closing_vert_V2_idx
= None
2003 # Determine which selection is Selection-U and which is Selection-V
2006 points_first_stroke_tips
= []
2009 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
2012 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2015 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2018 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2020 points_first_stroke_tips
.append(
2021 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2023 points_first_stroke_tips
.append(
2024 self
.main_splines
.data
.splines
[0].bezier_points
[
2025 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2029 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2030 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2032 if angle_A
< angle_B
:
2033 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2034 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2036 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2037 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2039 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2040 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2041 last_sketched_point_first_stroke_co
= \
2042 self
.main_splines
.data
.splines
[0].bezier_points
[
2043 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2045 first_sketched_point_last_stroke_co
= \
2046 self
.main_splines
.data
.splines
[
2047 len(self
.main_splines
.data
.splines
) - 1
2048 ].bezier_points
[0].co
2049 if len(self
.main_splines
.data
.splines
) > 1:
2050 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2051 last_sketched_point_second_stroke_co
= \
2052 self
.main_splines
.data
.splines
[1].bezier_points
[
2053 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2056 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2057 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2058 single_unselected_neighbors
.append(verts_neig_idx
[1])
2059 single_unselected_neighbors
.append(verts_neig_idx
[2])
2061 all_chains_tips_and_middle_vert
= []
2062 for v_idx
in all_chains_tips_idx
:
2063 if v_idx
not in single_unselected_neighbors
:
2064 all_chains_tips_and_middle_vert
.append(v_idx
)
2066 all_chains_tips_and_middle_vert
+= single_unselected_verts
2068 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2070 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2071 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2072 self
.shortest_distance(
2074 first_sketched_point_first_stroke_co
,
2075 all_chains_tips_and_middle_vert
2077 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2078 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2079 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2081 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2083 nearest_tip_to_first_st_first_pt_idx
,
2084 verts_tips_same_chain_idx
2086 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2087 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2088 self
.shortest_distance(
2090 last_sketched_point_first_stroke_co
,
2091 all_chains_tips_and_middle_vert
2093 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2094 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2095 self
.shortest_distance(
2097 first_sketched_point_last_stroke_co
,
2098 all_chains_tips_and_middle_vert
2100 if len(self
.main_splines
.data
.splines
) > 1:
2101 # The selected vertex nearest to the first point of the second sketched stroke
2102 # (This will be useful to determine the direction of the closed
2103 # selection V when extruding along strokes)
2104 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2105 self
.shortest_distance(
2107 first_sketched_point_second_stroke_co
,
2110 # The selected vertex nearest to the first point of the second sketched stroke
2111 # (This will be useful to determine the direction of the closed
2112 # selection V2 when extruding along strokes)
2113 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2114 self
.shortest_distance(
2116 last_sketched_point_second_stroke_co
,
2119 # Determine if the single selection will be treated as U or as V
2121 for i
in all_selected_edges_idx
:
2123 (self
.main_object
.matrix_world
@
2124 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2125 (self
.main_object
.matrix_world
@
2126 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2129 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2131 # Get shortest distance from the first point of the last stroke to any participating vertex
2132 _temp_idx
, shortest_distance_to_last_stroke
= \
2133 self
.shortest_distance(
2135 first_sketched_point_last_stroke_co
,
2136 all_participating_verts
2138 # If the beginning of the first stroke is near enough, and its orientation
2139 # difference with the first edge of the nearest selection chain is not too high,
2140 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2141 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2142 shortest_distance_to_last_stroke
< average_edge_length
and \
2143 len(self
.main_splines
.data
.splines
) > 1:
2145 self
.selection_U_exists
= False
2146 self
.selection_V_exists
= True
2147 # If the first selection is not closed
2148 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2149 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2150 self
.selection_V_is_closed
= False
2151 closing_vert_U_idx
= None
2152 closing_vert_U2_idx
= None
2153 closing_vert_V_idx
= None
2154 closing_vert_V2_idx
= None
2156 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2158 if selection_type
== "TWO_NOT_CONNECTED":
2159 self
.selection_V2_exists
= True
2161 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2163 self
.selection_V_is_closed
= True
2164 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2166 # Get the neighbors of the first (unselected) vert of the closed selection U.
2168 for verts
in single_unselected_verts_and_neighbors
:
2169 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2170 vert_neighbors
.append(verts
[1])
2171 vert_neighbors
.append(verts
[2])
2174 verts_V
= self
.get_ordered_verts(
2175 self
.main_object
, all_selected_edges_idx
,
2176 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2179 for i
in range(0, len(verts_V
)):
2180 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2181 # If the vertex nearest to the first point of the second stroke
2182 # is in the first half of the selected verts
2183 if i
>= len(verts_V
) / 2:
2184 first_vert_V_idx
= vert_neighbors
[1]
2187 first_vert_V_idx
= vert_neighbors
[0]
2190 if selection_type
== "TWO_NOT_CONNECTED":
2191 self
.selection_V2_exists
= True
2192 # If the second selection is not closed
2193 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2194 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2196 self
.selection_V2_is_closed
= False
2197 closing_vert_V2_idx
= None
2198 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2201 self
.selection_V2_is_closed
= True
2202 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2204 # Get the neighbors of the first (unselected) vert of the closed selection U
2206 for verts
in single_unselected_verts_and_neighbors
:
2207 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2208 vert_neighbors
.append(verts
[1])
2209 vert_neighbors
.append(verts
[2])
2212 verts_V2
= self
.get_ordered_verts(
2213 self
.main_object
, all_selected_edges_idx
,
2214 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2217 for i
in range(0, len(verts_V2
)):
2218 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2219 # If the vertex nearest to the first point of the second stroke
2220 # is in the first half of the selected verts
2221 if i
>= len(verts_V2
) / 2:
2222 first_vert_V2_idx
= vert_neighbors
[1]
2225 first_vert_V2_idx
= vert_neighbors
[0]
2228 self
.selection_V2_exists
= False
2231 self
.selection_U_exists
= True
2232 self
.selection_V_exists
= False
2233 # If the first selection is not closed
2234 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2235 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2236 self
.selection_U_is_closed
= False
2237 closing_vert_U_idx
= None
2241 self
.main_object
.matrix_world
@
2242 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2245 self
.main_object
.matrix_world
@
2246 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2248 points_first_stroke_tips
= []
2249 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2250 points_first_stroke_tips
.append(
2251 self
.main_splines
.data
.splines
[0].bezier_points
[
2252 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2255 vec_A
= points_tips
[0] - points_tips
[1]
2256 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2258 # Compare the direction of the selection and the first
2259 # grease pencil stroke to determine which is the "first" vertex of the selection
2260 if vec_A
.dot(vec_B
) < 0:
2261 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2263 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2266 self
.selection_U_is_closed
= True
2267 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2269 # Get the neighbors of the first (unselected) vert of the closed selection U
2271 for verts
in single_unselected_verts_and_neighbors
:
2272 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2273 vert_neighbors
.append(verts
[1])
2274 vert_neighbors
.append(verts
[2])
2277 points_first_and_neighbor
= []
2278 points_first_and_neighbor
.append(
2279 self
.main_object
.matrix_world
@
2280 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2282 points_first_and_neighbor
.append(
2283 self
.main_object
.matrix_world
@
2284 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2286 points_first_stroke_tips
= []
2287 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2288 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2290 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2291 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2293 # Compare the direction of the selection and the first grease pencil stroke to
2294 # determine which is the vertex neighbor to the first vertex (unselected) of
2295 # the closed selection. This will determine the direction of the closed selection
2296 if vec_A
.dot(vec_B
) < 0:
2297 first_vert_U_idx
= vert_neighbors
[1]
2299 first_vert_U_idx
= vert_neighbors
[0]
2301 if selection_type
== "TWO_NOT_CONNECTED":
2302 self
.selection_U2_exists
= True
2303 # If the second selection is not closed
2304 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2305 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2307 self
.selection_U2_is_closed
= False
2308 closing_vert_U2_idx
= None
2309 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2311 self
.selection_U2_is_closed
= True
2312 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2314 # Get the neighbors of the first (unselected) vert of the closed selection U
2316 for verts
in single_unselected_verts_and_neighbors
:
2317 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2318 vert_neighbors
.append(verts
[1])
2319 vert_neighbors
.append(verts
[2])
2322 points_first_and_neighbor
= []
2323 points_first_and_neighbor
.append(
2324 self
.main_object
.matrix_world
@
2325 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2327 points_first_and_neighbor
.append(
2328 self
.main_object
.matrix_world
@
2329 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2331 points_last_stroke_tips
= []
2332 points_last_stroke_tips
.append(
2333 self
.main_splines
.data
.splines
[
2334 len(self
.main_splines
.data
.splines
) - 1
2335 ].bezier_points
[0].co
2337 points_last_stroke_tips
.append(
2338 self
.main_splines
.data
.splines
[
2339 len(self
.main_splines
.data
.splines
) - 1
2340 ].bezier_points
[1].co
2342 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2343 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2345 # Compare the direction of the selection and the last grease pencil stroke to
2346 # determine which is the vertex neighbor to the first vertex (unselected) of
2347 # the closed selection. This will determine the direction of the closed selection
2348 if vec_A
.dot(vec_B
) < 0:
2349 first_vert_U2_idx
= vert_neighbors
[1]
2351 first_vert_U2_idx
= vert_neighbors
[0]
2353 self
.selection_U2_exists
= False
2355 elif selection_type
== "NO_SELECTION":
2356 self
.selection_U_exists
= False
2357 self
.selection_V_exists
= False
2359 # Get an ordered list of the vertices of Selection-U
2360 verts_ordered_U
= []
2361 if self
.selection_U_exists
:
2362 verts_ordered_U
= self
.get_ordered_verts(
2363 self
.main_object
, all_selected_edges_idx
,
2364 all_verts_idx
, first_vert_U_idx
,
2365 middle_vertex_idx
, closing_vert_U_idx
2368 # Get an ordered list of the vertices of Selection-U2
2369 verts_ordered_U2
= []
2370 if self
.selection_U2_exists
:
2371 verts_ordered_U2
= self
.get_ordered_verts(
2372 self
.main_object
, all_selected_edges_idx
,
2373 all_verts_idx
, first_vert_U2_idx
,
2374 middle_vertex_idx
, closing_vert_U2_idx
2377 # Get an ordered list of the vertices of Selection-V
2378 verts_ordered_V
= []
2379 if self
.selection_V_exists
:
2380 verts_ordered_V
= self
.get_ordered_verts(
2381 self
.main_object
, all_selected_edges_idx
,
2382 all_verts_idx
, first_vert_V_idx
,
2383 middle_vertex_idx
, closing_vert_V_idx
2385 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2387 # Get an ordered list of the vertices of Selection-V2
2388 verts_ordered_V2
= []
2389 if self
.selection_V2_exists
:
2390 verts_ordered_V2
= self
.get_ordered_verts(
2391 self
.main_object
, all_selected_edges_idx
,
2392 all_verts_idx
, first_vert_V2_idx
,
2393 middle_vertex_idx
, closing_vert_V2_idx
2396 # Check if when there are two-not-connected selections both have the same
2397 # number of verts. If not terminate the script
2398 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2399 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2401 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2403 self
.stopping_errors
= True
2407 # Calculate edges U proportions
2408 # Sum selected edges U lengths
2409 edges_lengths_U
= []
2410 edges_lengths_sum_U
= 0
2412 if self
.selection_U_exists
:
2413 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2417 if self
.selection_U2_exists
:
2418 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2422 # Sum selected edges V lengths
2423 edges_lengths_V
= []
2424 edges_lengths_sum_V
= 0
2426 if self
.selection_V_exists
:
2427 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2431 if self
.selection_V2_exists
:
2432 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2437 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2438 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2439 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2440 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2443 edges_proportions_U
= []
2444 edges_proportions_U
= self
.get_edges_proportions(
2445 edges_lengths_U
, edges_lengths_sum_U
,
2446 self
.selection_U_exists
, self
.edges_U
2448 verts_count_U
= len(edges_proportions_U
) + 1
2450 if self
.selection_U2_exists
:
2451 edges_proportions_U2
= []
2452 edges_proportions_U2
= self
.get_edges_proportions(
2453 edges_lengths_U2
, edges_lengths_sum_U2
,
2454 self
.selection_U2_exists
, self
.edges_V
2458 edges_proportions_V
= []
2459 edges_proportions_V
= self
.get_edges_proportions(
2460 edges_lengths_V
, edges_lengths_sum_V
,
2461 self
.selection_V_exists
, self
.edges_V
2464 if self
.selection_V2_exists
:
2465 edges_proportions_V2
= []
2466 edges_proportions_V2
= self
.get_edges_proportions(
2467 edges_lengths_V2
, edges_lengths_sum_V2
,
2468 self
.selection_V2_exists
, self
.edges_V
2471 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2472 # the actual sketched curves with a "closing segment"
2473 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2474 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2475 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2477 simplified_spline_coords
= []
2478 simplified_curve
= []
2479 ob_simplified_curve
= []
2480 splines_first_v_co
= []
2481 for i
in range(len(self
.main_splines
.data
.splines
)):
2482 # Create a curve object for the actual spline "cyclic extension"
2483 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2484 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2485 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2487 simplified_curve
[i
].dimensions
= "3D"
2490 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2491 spline_coords
.append(bp
.co
)
2494 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2496 # Get the coordinates of the first vert of the actual spline
2497 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2499 # Generate the spline
2500 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2501 # less one because one point is added when the spline is created
2502 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2503 for p
in range(0, len(simplified_spline_coords
[i
])):
2504 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2506 spline
.use_cyclic_u
= True
2508 spline_bp_count
= len(spline
.bezier_points
)
2510 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2511 ob_simplified_curve
[i
].select_set(True)
2512 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2514 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2515 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2516 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2517 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2518 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2520 # Select the "closing segment", and subdivide it
2521 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2522 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2523 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2525 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2526 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2527 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2529 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2531 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2532 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2533 self
.average_gp_segment_length
2536 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2538 # Delete the other vertices and make it non-cyclic to
2539 # keep only the needed verts of the "closing segment"
2540 bpy
.ops
.curve
.select_all(action
='INVERT')
2541 bpy
.ops
.curve
.delete(type='VERT')
2542 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2543 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2545 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2546 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2547 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2548 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2550 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2551 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2552 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2554 # Delete the temporal curve
2555 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2557 # Get the coords of the points distributed along the sketched strokes,
2558 # with proportions-U of the first selection
2559 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2560 self
.main_splines
.data
.splines
,
2563 sketched_splines_parsed
= []
2565 if self
.selection_U2_exists
:
2566 # Initialize the multidimensional list with the proportions of all the segments
2567 proportions_loops_crossing_strokes
= []
2568 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2569 proportions_loops_crossing_strokes
.append([])
2571 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2572 proportions_loops_crossing_strokes
[i
].append(None)
2574 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2575 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2576 loop_segments_lengths
= []
2578 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2579 # When on the first stroke, add the segment from the selection to the dirst stroke
2581 loop_segments_lengths
.append(
2582 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2583 pts_on_strokes_with_proportions_U
[0][lp
]).length
2585 # For all strokes except for the last, calculate the distance
2586 # from the actual stroke to the next
2587 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2588 loop_segments_lengths
.append(
2589 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2590 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2592 # When on the last stroke, add the segments
2593 # from the last stroke to the second selection
2594 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2595 loop_segments_lengths
.append(
2596 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2597 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2599 # Calculate full loop length
2600 loop_seg_lengths_sum
= 0
2601 for i
in range(len(loop_segments_lengths
)):
2602 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2604 # Fill the multidimensional list with the proportions of all the segments
2605 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2606 proportions_loops_crossing_strokes
[st
][lp
] = \
2607 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2609 # Calculate proportions for each stroke
2610 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2611 actual_stroke_spline
= []
2612 # Needs to be a list for the "distribute_pts" method
2613 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2615 # Calculate the proportions for the actual stroke.
2616 actual_edges_proportions_U
= []
2617 for i
in range(len(edges_proportions_U
)):
2620 # Sum the proportions of this loop up to the actual.
2621 for t
in range(0, st
+ 1):
2622 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2623 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2624 # and the proportions refer to edges, so we start at the element 1
2625 # of proportions_loops_crossing_strokes instead of element 0
2626 actual_edges_proportions_U
.append(
2627 edges_proportions_U
[i
] -
2628 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2630 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2631 sketched_splines_parsed
.append(points_actual_spline
[0])
2633 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2635 # If the selection type is "TWO_NOT_CONNECTED" replace the
2636 # points of the last spline with the points in the "target" selection
2637 if selection_type
== "TWO_NOT_CONNECTED":
2638 if self
.selection_U2_exists
:
2639 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2640 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2641 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2643 # Create temporary curves along the "control-points" found
2644 # on the sketched curves and the mesh selection
2645 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2646 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2647 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2648 ob_ctrl_pts
.data
= me
2649 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2656 for i
in range(0, verts_count_U
):
2657 vert_num_in_spline
= 1
2659 if self
.selection_U_exists
:
2660 ob_ctrl_pts
.data
.vertices
.add(1)
2661 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2662 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2664 vert_num_in_spline
+= 1
2666 for t
in range(0, len(sketched_splines_parsed
)):
2667 ob_ctrl_pts
.data
.vertices
.add(1)
2668 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2669 v
.co
= sketched_splines_parsed
[t
][i
]
2671 if vert_num_in_spline
> 1:
2672 ob_ctrl_pts
.data
.edges
.add(1)
2673 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2674 len(ob_ctrl_pts
.data
.vertices
) - 2
2675 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2676 len(ob_ctrl_pts
.data
.vertices
) - 1
2679 first_verts
.append(v
.index
)
2682 second_verts
.append(v
.index
)
2684 if t
== len(sketched_splines_parsed
) - 1:
2685 last_verts
.append(v
.index
)
2688 vert_num_in_spline
+= 1
2690 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2691 ob_ctrl_pts
.select_set(True)
2692 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2694 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2695 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2696 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2698 # Determine which loops-U will be "Cyclic"
2699 for i
in range(0, len(first_verts
)):
2700 # When there is Cyclic Cross there is no need of
2701 # Automatic Join, (and there are at least three strokes)
2702 if self
.automatic_join
and not self
.cyclic_cross
and \
2703 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2705 v
= ob_ctrl_pts
.data
.vertices
2706 first_point_co
= v
[first_verts
[i
]].co
2707 second_point_co
= v
[second_verts
[i
]].co
2708 last_point_co
= v
[last_verts
[i
]].co
2710 # Coordinates of the point in the center of both the first and last verts.
2712 (first_point_co
[0] + last_point_co
[0]) / 2,
2713 (first_point_co
[1] + last_point_co
[1]) / 2,
2714 (first_point_co
[2] + last_point_co
[2]) / 2
2716 vec_A
= second_point_co
- first_point_co
2717 vec_B
= second_point_co
- Vector(verts_center_co
)
2719 # Calculate the length of the first segment of the loop,
2720 # and the length it would have after moving the first vert
2721 # to the middle position between first and last
2722 length_original
= (second_point_co
- first_point_co
).length
2723 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2725 angle
= vec_A
.angle(vec_B
) / pi
2727 # If the target length doesn't stretch too much, and the
2728 # its angle doesn't change to much either
2729 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2730 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2732 cyclic_loops_U
.append(True)
2733 # Move the first vert to the center coordinates
2734 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2735 # Select the last verts from Cyclic loops, for later deletion all at once
2736 v
[last_verts
[i
]].select
= True
2738 cyclic_loops_U
.append(False)
2740 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2741 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2742 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2743 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2745 cyclic_loops_U
.append(True)
2747 cyclic_loops_U
.append(False)
2749 # The cyclic_loops_U list needs to be reversed.
2750 cyclic_loops_U
.reverse()
2752 # Delete the previously selected (last_)verts.
2753 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2754 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2755 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2757 # Create curves from control points.
2758 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2759 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2760 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2761 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2762 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2764 # Make Cyclic the splines designated as Cyclic.
2765 for i
in range(0, len(cyclic_loops_U
)):
2766 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2768 # Get the coords of all points on first loop-U, for later comparison with its
2769 # subdivided version, to know which points of the loops-U are crossed by the
2770 # original strokes. The indices will be the same for the other loops-U
2771 if self
.loops_on_strokes
:
2772 coords_loops_U_control_points
= []
2773 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2774 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2776 tuple(coords_loops_U_control_points
)
2778 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2779 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2780 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2782 edges_V_count
= len(edges_proportions_V
)
2784 # The Follow precision will vary depending on the number of Follow face-loops
2785 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2786 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2788 # Subdivide the curves
2789 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2791 # The verts position shifting that happens with splines subdivision.
2792 # For later reorder splines points
2793 verts_position_shift
= curve_cuts
+ 1
2794 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2796 # Reorder coordinates of the points of each spline to put the first point of
2797 # the spline starting at the position it was the first point before sudividing
2798 # the curve. And make a new curve object per spline (to handle memory better later)
2799 splines_U_objects
= []
2800 for i
in range(len(ob_curves_surf
.data
.splines
)):
2801 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2802 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2803 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2805 spline_U_curve
.dimensions
= "3D"
2807 # Add points to the spline in the new curve object
2808 ob_spline_U
.data
.splines
.new('BEZIER')
2809 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2810 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2811 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2812 point_index
= t
+ verts_position_shift
2814 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2817 # to avoid adding the first point since it's added when the spline is created
2819 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2820 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2821 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2823 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2824 # Add a last point at the same location as the first one
2825 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2826 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2827 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2829 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2831 splines_U_objects
.append(ob_spline_U
)
2832 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2833 ob_spline_U
.select_set(True)
2834 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2836 # When option "Loops on strokes" is active each "Cross" loop will have
2837 # its own proportions according to where the original strokes "touch" them
2838 if self
.loops_on_strokes
:
2839 # Get the indices of points where the original strokes "touch" loops-U
2840 points_U_crossed_by_strokes
= []
2841 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2842 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2843 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2844 points_U_crossed_by_strokes
.append(i
)
2846 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2847 edge_order_number_for_splines
= {}
2848 if self
.selection_V_exists
:
2849 # For two-connected selections add a first hypothetic stroke at the beginning.
2850 if selection_type
== "TWO_CONNECTED":
2851 edge_order_number_for_splines
[0] = 0
2853 for i
in range(len(self
.main_splines
.data
.splines
)):
2854 sp
= self
.main_splines
.data
.splines
[i
]
2855 v_idx
, _dist_temp
= self
.shortest_distance(
2857 sp
.bezier_points
[0].co
,
2858 verts_ordered_V_indices
2860 # Get the position (edges count) of the vert v_idx in the selected chain V
2861 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2863 # For two-connected selections the strokes go after the
2864 # hypothetic stroke added before, so the index adds one per spline
2865 if selection_type
== "TWO_CONNECTED":
2866 spline_number
= i
+ 1
2870 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2872 # Get the first and last verts indices for later comparison
2875 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2878 if self
.selection_V_is_closed
:
2879 # If there is no last stroke on the last vertex (same as first vertex),
2880 # add a hypothetic spline at last vert order
2881 if first_v_idx
!= last_v_idx
:
2882 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2883 len(verts_ordered_V_indices
) - 1
2885 if self
.cyclic_cross
:
2886 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2887 len(verts_ordered_V_indices
) - 2
2888 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2889 len(verts_ordered_V_indices
) - 1
2891 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2892 len(verts_ordered_V_indices
) - 1
2894 # Get the coords of the points distributed along the
2895 # "crossing curves", with appropriate proportions-V
2896 surface_splines_parsed
= []
2897 for i
in range(len(splines_U_objects
)):
2898 sp_ob
= splines_U_objects
[i
]
2899 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2900 if self
.loops_on_strokes
:
2901 # Segments distances from stroke to stroke
2904 segments_distances
= []
2905 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2906 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2912 dist
+= (last_p
- actual_p
).length
2914 if t
in points_U_crossed_by_strokes
:
2915 segments_distances
.append(dist
)
2922 # Calculate Proportions.
2923 used_edges_proportions_V
= []
2924 for t
in range(len(segments_distances
)):
2925 if self
.selection_V_exists
:
2927 order_number_last_stroke
= 0
2929 segment_edges_length_V
= 0
2930 segment_edges_length_V2
= 0
2931 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2932 segment_edges_length_V
+= edges_lengths_V
[order
]
2933 if self
.selection_V2_exists
:
2934 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2936 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2937 # Calculate each "sub-segment" (the ones between each stroke) length
2938 if self
.selection_V2_exists
:
2939 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2940 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2941 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2942 (segment_edges_length_V2
- segment_edges_length_V
) /
2943 len(splines_U_objects
) * i
)
2945 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2947 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2948 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2950 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2952 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2955 for _c
in range(self
.edges_V
):
2956 # Calculate each "sub-segment" (the ones between each stroke) length
2957 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2958 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2960 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2961 surface_splines_parsed
.append(actual_spline
[0])
2964 if self
.selection_V2_exists
:
2965 used_edges_proportions_V
= []
2966 for p
in range(len(edges_proportions_V
)):
2967 used_edges_proportions_V
.append(
2968 edges_proportions_V2
[p
] -
2969 ((edges_proportions_V2
[p
] -
2970 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2973 used_edges_proportions_V
= edges_proportions_V
2975 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2976 surface_splines_parsed
.append(actual_spline
[0])
2978 # Set the verts of the first and last splines to the locations
2979 # of the respective verts in the selections
2980 if self
.selection_V_exists
:
2981 for i
in range(0, len(surface_splines_parsed
[0])):
2982 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2983 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2985 if selection_type
== "TWO_NOT_CONNECTED":
2986 if self
.selection_V2_exists
:
2987 for i
in range(0, len(surface_splines_parsed
[0])):
2988 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2990 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2991 # merge the verts of the tips of the loops when they are "near enough"
2992 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2993 # Join the tips of "Follow" loops that are near enough and must be "closed"
2994 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2995 for i
in range(len(surface_splines_parsed
[0])):
2996 sp
= surface_splines_parsed
2997 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2999 verts_middle_position_co
= [
3000 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
3001 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
3002 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
3004 points_original
= []
3005 points_original
.append(sp
[1][i
])
3006 points_original
.append(sp
[0][i
])
3009 points_target
.append(sp
[1][i
])
3010 points_target
.append(Vector(verts_middle_position_co
))
3012 vec_A
= points_original
[0] - points_original
[1]
3013 vec_B
= points_target
[0] - points_target
[1]
3014 # check for zero angles, not sure if it is a great fix
3015 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3016 angle
= vec_A
.angle(vec_B
) / pi
3017 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3022 # If after moving the verts to the middle point, the segment doesn't stretch too much
3023 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3024 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3026 # Avoid joining when the actual loop must be merged with the original mesh
3027 if not (self
.selection_U_exists
and i
== 0) and \
3028 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3030 # Change the coords of both verts to the middle position
3031 surface_splines_parsed
[0][i
] = verts_middle_position_co
3032 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3034 # Delete object with control points and object from grease pencil conversion
3035 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3037 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3041 # Get all verts coords
3042 all_surface_verts_co
= []
3043 for i
in range(0, len(surface_splines_parsed
)):
3044 # Get coords of all verts and make a list with them
3045 for pt_co
in surface_splines_parsed
[i
]:
3046 all_surface_verts_co
.append(pt_co
)
3048 # Define verts for each face
3049 all_surface_faces
= []
3050 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3051 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3052 all_surface_faces
.append(
3053 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3054 i
+ len(surface_splines_parsed
[0]) + 1]
3057 surf_me_name
= "SURFSKIO_surface"
3058 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3059 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3060 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3061 ob_surface
.location
= (0.0, 0.0, 0.0)
3062 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3063 ob_surface
.scale
= (1.0, 1.0, 1.0)
3065 # Select all the "unselected but participating" verts, from closed selection
3066 # or double selections with middle-vertex, for later join with remove doubles
3067 for v_idx
in single_unselected_verts
:
3068 self
.main_object
.data
.vertices
[v_idx
].select
= True
3070 # Join the new mesh to the main object
3071 ob_surface
.select_set(True)
3072 self
.main_object
.select_set(True)
3073 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3075 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3077 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3079 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3080 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3081 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3089 global global_shade_smooth
3090 if global_shade_smooth
:
3091 bpy
.ops
.object.shade_smooth()
3093 bpy
.ops
.object.shade_flat()
3094 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3100 def execute(self
, context
):
3102 if bpy
.ops
.object.mode_set
.poll():
3103 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3106 global global_mesh_object
3107 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3108 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3109 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3110 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3111 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3113 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3115 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3119 if not self
.is_fill_faces
:
3120 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3121 value
='True, False, False')
3123 # Build splines from the "last saved splines".
3124 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3125 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3126 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3128 last_saved_curve
.dimensions
= "3D"
3130 for sp
in self
.last_strokes_splines_coords
:
3131 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3132 # less one because one point is added when the spline is created
3133 spline
.bezier_points
.add(len(sp
) - 1)
3134 for p
in range(0, len(sp
)):
3135 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3137 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3139 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3140 self
.main_splines
.select_set(True)
3141 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3143 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3145 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3146 # Important to make it vector first and then automatic, otherwise the
3147 # tips handles get too big and distort the shrinkwrap results later
3148 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3149 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3150 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3151 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3153 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3155 if self
.is_crosshatch
:
3156 strokes_for_crosshatch
= True
3157 strokes_for_rectangular_surface
= False
3159 strokes_for_rectangular_surface
= True
3160 strokes_for_crosshatch
= False
3162 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3164 if strokes_for_rectangular_surface
:
3165 self
.rectangular_surface(context
)
3166 elif strokes_for_crosshatch
:
3167 self
.crosshatch_surface_execute(context
)
3169 #Set Shade smooth to new polygons
3170 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3171 global global_shade_smooth
3172 if global_shade_smooth
:
3173 bpy
.ops
.object.shade_smooth()
3175 bpy
.ops
.object.shade_flat()
3177 # Delete main splines
3178 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3179 if self
.keep_strokes
:
3180 self
.main_splines
.name
= "keep_strokes"
3181 self
.main_splines
.data
.bevel_depth
= 0.001
3182 if "keep_strokes_material" in bpy
.data
.materials
:
3183 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3185 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3186 mat
.diffuse_color
= (1, 0, 0, 0)
3187 mat
.specular_color
= (1, 0, 0)
3188 mat
.specular_intensity
= 0.0
3190 self
.main_splines
.data
.materials
.append(mat
)
3192 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3194 # Delete grease pencil strokes
3195 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3197 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3201 # Delete annotations
3202 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3204 bpy
.context
.annotation_data
.layers
.active
.clear()
3208 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3209 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3210 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3211 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3212 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3213 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3214 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3215 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3217 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3218 self
.main_object
.select_set(True)
3219 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3221 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3227 def invoke(self
, context
, event
):
3229 if bpy
.ops
.object.mode_set
.poll():
3230 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3232 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3233 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3234 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3235 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3236 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3237 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3240 global global_mesh_object
3241 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3242 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3243 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3244 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3246 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3251 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3253 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3254 value
='True, False, False')
3256 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3257 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3259 self
.is_fill_faces
= False
3260 self
.stopping_errors
= False
3261 self
.last_strokes_splines_coords
= []
3263 # Determine the type of the strokes
3264 self
.strokes_type
= get_strokes_type(context
)
3266 # Check if it will be used grease pencil strokes or curves
3267 # If there are strokes to be used
3268 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3269 if self
.strokes_type
== "GP_STROKES":
3270 # Convert grease pencil strokes to curve
3271 global global_gpencil_object
3272 gp
= bpy
.data
.objects
[global_gpencil_object
]
3273 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3274 self
.using_external_curves
= False
3276 elif self
.strokes_type
== "GP_ANNOTATION":
3277 # Convert grease pencil strokes to curve
3278 gp
= bpy
.context
.annotation_data
3279 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3280 self
.using_external_curves
= False
3282 elif self
.strokes_type
== "EXTERNAL_CURVE":
3283 global global_curve_object
3284 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3285 self
.using_external_curves
= True
3287 # Make sure there are no objects left from erroneous
3288 # executions of this operator, with the reserved names used here
3289 for o
in bpy
.data
.objects
:
3290 if o
.name
.find("SURFSKIO_") != -1:
3291 bpy
.ops
.object.delete({"selected_objects": [o
]})
3293 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3295 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3297 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3299 # Deselect all points of the curve
3300 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3301 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3302 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3304 # Delete splines with only a single isolated point
3305 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3306 sp
= self
.temporary_curve
.data
.splines
[i
]
3308 if len(sp
.bezier_points
) == 1:
3309 sp
.bezier_points
[0].select_control_point
= True
3311 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3312 bpy
.ops
.curve
.delete(type='VERT')
3313 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3315 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3316 self
.temporary_curve
.select_set(True)
3317 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3319 # Set a minimum number of points for crosshatch
3320 minimum_points_num
= 15
3322 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3323 # Check if the number of points of each curve has at least the number of points
3324 # of minimum_points_num, which is a bit more than the face-loops limit.
3325 # If not, subdivide to reach at least that number of points
3326 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3327 sp
= self
.temporary_curve
.data
.splines
[i
]
3329 if len(sp
.bezier_points
) < minimum_points_num
:
3330 for bp
in sp
.bezier_points
:
3331 bp
.select_control_point
= True
3333 if (len(sp
.bezier_points
) - 1) != 0:
3334 # Formula to get the number of cuts that will make a curve
3335 # of N number of points have near to "minimum_points_num"
3336 # points, when subdividing with this number of cuts
3337 subdivide_cuts
= int(
3338 (minimum_points_num
- len(sp
.bezier_points
)) /
3339 (len(sp
.bezier_points
) - 1)
3344 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3345 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3347 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3349 # Detect if the strokes are a crosshatch and do it if it is
3350 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3352 if not self
.is_crosshatch
:
3353 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3354 self
.temporary_curve
.select_set(True)
3355 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3357 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3359 # Set a minimum number of points for rectangular surfaces
3360 minimum_points_num
= 60
3362 # Check if the number of points of each curve has at least the number of points
3363 # of minimum_points_num, which is a bit more than the face-loops limit.
3364 # If not, subdivide to reach at least that number of points
3365 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3366 sp
= self
.temporary_curve
.data
.splines
[i
]
3368 if len(sp
.bezier_points
) < minimum_points_num
:
3369 for bp
in sp
.bezier_points
:
3370 bp
.select_control_point
= True
3372 if (len(sp
.bezier_points
) - 1) != 0:
3373 # Formula to get the number of cuts that will make a curve of
3374 # N number of points have near to "minimum_points_num" points,
3375 # when subdividing with this number of cuts
3376 subdivide_cuts
= int(
3377 (minimum_points_num
- len(sp
.bezier_points
)) /
3378 (len(sp
.bezier_points
) - 1)
3383 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3384 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3386 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3388 # Save coordinates of the actual strokes (as the "last saved splines")
3389 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3390 self
.last_strokes_splines_coords
.append([])
3391 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3392 coords
= self
.temporary_curve
.matrix_world
@ \
3393 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3394 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3396 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3397 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3398 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3399 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3400 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3401 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3404 (first_p_co
[0] + last_p_co
[0]) / 2,
3405 (first_p_co
[1] + last_p_co
[1]) / 2,
3406 (first_p_co
[2] + last_p_co
[2]) / 2
3409 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3410 self
.last_strokes_splines_coords
[sp_idx
][
3411 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3413 tuple(self
.last_strokes_splines_coords
)
3415 # Estimation of the average length of the segments between
3416 # each point of the grease pencil strokes.
3417 # Will be useful to determine whether a curve should be made "Cyclic"
3418 segments_lengths_sum
= 0
3420 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3421 for i
in range(0, len(random_spline
)):
3422 if i
!= 0 and len(random_spline
) - 1 >= i
:
3423 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3426 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3428 # Delete temporary strokes curve object
3429 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3431 # Set again since "execute()" will turn it again to its initial value
3432 self
.execute(context
)
3434 if not self
.stopping_errors
:
3435 # Delete grease pencil strokes
3436 if self
.strokes_type
== "GP_STROKES":
3438 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3442 # Delete annotation strokes
3443 elif self
.strokes_type
== "GP_ANNOTATION":
3445 bpy
.context
.annotation_data
.layers
.active
.clear()
3449 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3450 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3451 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3457 elif self
.strokes_type
== "SELECTION_ALONE":
3458 self
.is_fill_faces
= True
3459 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3461 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3463 if created_faces_count
== 0:
3464 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3465 return {"CANCELLED"}
3469 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3470 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3473 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3474 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3477 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3478 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3480 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3483 elif self
.strokes_type
== "NO_STROKES":
3484 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3487 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3488 self
.report({'WARNING'}, "All splines must be Bezier.")
3494 # ----------------------------
3496 class MESH_OT_SURFSK_init(Operator
):
3497 bl_idname
= "mesh.surfsk_init"
3498 bl_label
= "Bsurfaces initialize"
3499 bl_description
= "Add an empty mesh object with useful settings"
3500 bl_options
= {'REGISTER', 'UNDO'}
3502 def execute(self
, context
):
3504 bs
= bpy
.context
.scene
.bsurfaces
3506 if bpy
.ops
.object.mode_set
.poll():
3507 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3509 global global_shade_smooth
3510 global global_mesh_object
3511 global global_gpencil_object
3513 if bs
.SURFSK_mesh
== None:
3514 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3515 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3516 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3517 mesh_object
.select_set(True)
3518 bpy
.context
.view_layer
.objects
.active
= mesh_object
3520 mesh_object
.show_all_edges
= True
3521 mesh_object
.display_type
= 'SOLID'
3522 mesh_object
.show_wire
= True
3524 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3525 if global_shade_smooth
:
3526 bpy
.ops
.object.shade_smooth()
3528 bpy
.ops
.object.shade_flat()
3530 color_red
= [1.0, 0.0, 0.0, 0.3]
3531 material
= makeMaterial("BSurfaceMesh", color_red
)
3532 mesh_object
.data
.materials
.append(material
)
3533 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3534 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3535 if self
.active_object
is not None:
3536 modifier
.target
= self
.active_object
3537 modifier
.wrap_method
= 'TARGET_PROJECT'
3538 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3539 modifier
.show_on_cage
= True
3541 global_mesh_object
= mesh_object
.name
3542 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3544 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3545 bpy
.context
.scene
.tool_settings
.use_snap
= True
3546 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3547 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3548 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3549 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3550 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3552 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3553 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3555 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3556 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3557 bpy
.ops
.object.gpencil_add(radius
=1.0, align
='WORLD', location
=(0.0, 0.0, 0.0), rotation
=(0.0, 0.0, 0.0), type='EMPTY')
3558 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3559 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3560 gpencil_object
.select_set(True)
3561 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3562 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3563 global_gpencil_object
= gpencil_object
.name
3564 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3565 gpencil_object
.data
.stroke_depth_order
= '3D'
3566 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3567 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3569 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3570 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3571 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3573 def invoke(self
, context
, event
):
3574 if bpy
.context
.active_object
:
3575 self
.active_object
= bpy
.context
.active_object
3577 self
.active_object
= None
3579 self
.execute(context
)
3583 # ----------------------------
3584 # Add modifiers operator
3585 class MESH_OT_SURFSK_add_modifiers(Operator
):
3586 bl_idname
= "mesh.surfsk_add_modifiers"
3587 bl_label
= "Add Mirror and others modifiers"
3588 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3589 bl_options
= {'REGISTER', 'UNDO'}
3591 def execute(self
, context
):
3593 bs
= bpy
.context
.scene
.bsurfaces
3595 if bpy
.ops
.object.mode_set
.poll():
3596 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3598 if bs
.SURFSK_mesh
== None:
3599 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3601 mesh_object
= bs
.SURFSK_mesh
3604 mesh_object
.select_set(True)
3606 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3609 bpy
.context
.view_layer
.objects
.active
= mesh_object
3612 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3613 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3614 shrinkwrap
.target
= self
.active_object
3615 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3616 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3617 shrinkwrap
.show_on_cage
= True
3618 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3620 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3621 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3622 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3623 shrinkwrap
.target
= self
.active_object
3624 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3625 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3626 shrinkwrap
.show_on_cage
= True
3627 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3630 mirror
= mesh_object
.modifiers
["Mirror"]
3631 mirror
.use_clip
= True
3633 bpy
.ops
.object.modifier_add(type='MIRROR')
3634 mirror
= mesh_object
.modifiers
["Mirror"]
3635 mirror
.use_clip
= True
3638 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3640 bpy
.ops
.object.modifier_add(type='SUBSURF')
3641 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3644 solidify
= mesh_object
.modifiers
["Solidify"]
3645 solidify
.thickness
= 0.01
3647 bpy
.ops
.object.modifier_add(type='SOLIDIFY')
3648 solidify
= mesh_object
.modifiers
["Solidify"]
3649 solidify
.thickness
= 0.01
3653 def invoke(self
, context
, event
):
3654 if bpy
.context
.active_object
:
3655 self
.active_object
= bpy
.context
.active_object
3657 self
.active_object
= None
3659 self
.execute(context
)
3663 # ----------------------------
3664 # Edit surface operator
3665 class MESH_OT_SURFSK_edit_surface(Operator
):
3666 bl_idname
= "mesh.surfsk_edit_surface"
3667 bl_label
= "Bsurfaces edit surface"
3668 bl_description
= "Edit surface mesh"
3669 bl_options
= {'REGISTER', 'UNDO'}
3671 def execute(self
, context
):
3672 if bpy
.ops
.object.mode_set
.poll():
3673 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3674 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3675 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3676 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3677 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3678 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3680 def invoke(self
, context
, event
):
3682 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3683 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3684 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3685 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3687 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3690 self
.execute(context
)
3694 # ----------------------------
3695 # Add strokes operator
3696 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3697 bl_idname
= "gpencil.surfsk_add_strokes"
3698 bl_label
= "Bsurfaces add strokes"
3699 bl_description
= "Add the grease pencil strokes"
3700 bl_options
= {'REGISTER', 'UNDO'}
3702 def execute(self
, context
):
3703 if bpy
.ops
.object.mode_set
.poll():
3704 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3705 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3707 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3708 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3709 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3710 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3714 def invoke(self
, context
, event
):
3716 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3718 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3721 self
.execute(context
)
3725 # ----------------------------
3726 # Edit strokes operator
3727 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3728 bl_idname
= "gpencil.surfsk_edit_strokes"
3729 bl_label
= "Bsurfaces edit strokes"
3730 bl_description
= "Edit the grease pencil strokes"
3731 bl_options
= {'REGISTER', 'UNDO'}
3733 def execute(self
, context
):
3734 if bpy
.ops
.object.mode_set
.poll():
3735 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3736 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3738 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3740 gpencil_object
.select_set(True)
3741 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3743 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3745 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3749 def invoke(self
, context
, event
):
3751 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3753 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3756 self
.execute(context
)
3760 # ----------------------------
3761 # Convert annotation to curves operator
3762 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3763 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3764 bl_label
= "Convert annotation to curves"
3765 bl_description
= "Convert annotation to curves for editing"
3766 bl_options
= {'REGISTER', 'UNDO'}
3768 def execute(self
, context
):
3770 if bpy
.ops
.object.mode_set
.poll():
3771 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3773 # Convert annotation to curve
3774 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3777 # Delete annotation strokes
3779 bpy
.context
.annotation_data
.layers
.active
.clear()
3784 curve
.select_set(True)
3785 bpy
.context
.view_layer
.objects
.active
= curve
3787 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3791 def invoke(self
, context
, event
):
3793 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3795 _strokes_num
= len(strokes
)
3797 self
.report({'WARNING'}, "Not active annotation")
3800 self
.execute(context
)
3804 # ----------------------------
3805 # Convert strokes to curves operator
3806 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3807 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3808 bl_label
= "Convert strokes to curves"
3809 bl_description
= "Convert grease pencil strokes to curves for editing"
3810 bl_options
= {'REGISTER', 'UNDO'}
3812 def execute(self
, context
):
3814 if bpy
.ops
.object.mode_set
.poll():
3815 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3817 # Convert grease pencil strokes to curve
3818 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3819 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3822 # Delete grease pencil strokes
3824 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3830 curve
.select_set(True)
3831 bpy
.context
.view_layer
.objects
.active
= curve
3833 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3837 def invoke(self
, context
, event
):
3839 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3841 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3844 self
.execute(context
)
3848 # ----------------------------
3850 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3851 bl_idname
= "gpencil.surfsk_add_annotation"
3852 bl_label
= "Bsurfaces add annotation"
3853 bl_description
= "Add annotation"
3854 bl_options
= {'REGISTER', 'UNDO'}
3856 def execute(self
, context
):
3857 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3858 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3862 def invoke(self
, context
, event
):
3864 self
.execute(context
)
3869 # ----------------------------
3870 # Edit curve operator
3871 class CURVE_OT_SURFSK_edit_curve(Operator
):
3872 bl_idname
= "curve.surfsk_edit_curve"
3873 bl_label
= "Bsurfaces edit curve"
3874 bl_description
= "Edit curve"
3875 bl_options
= {'REGISTER', 'UNDO'}
3877 def execute(self
, context
):
3878 if bpy
.ops
.object.mode_set
.poll():
3879 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3880 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3881 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3882 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3883 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3885 def invoke(self
, context
, event
):
3887 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3889 self
.report({'WARNING'}, "Specify the name of the object with curve")
3892 self
.execute(context
)
3896 # ----------------------------
3898 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3899 bl_idname
= "curve.surfsk_reorder_splines"
3900 bl_label
= "Bsurfaces reorder splines"
3901 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3902 bl_options
= {'REGISTER', 'UNDO'}
3904 def execute(self
, context
):
3905 objects_to_delete
= []
3906 # Convert grease pencil strokes to curve.
3907 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3908 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3909 for ob
in bpy
.context
.selected_objects
:
3910 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3911 GP_strokes_curve
= ob
3913 # GP_strokes_curve = bpy.context.object
3914 objects_to_delete
.append(GP_strokes_curve
)
3916 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3917 GP_strokes_curve
.select_set(True)
3918 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3920 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3921 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3922 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3923 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3925 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3926 GP_strokes_mesh
= bpy
.context
.object
3927 objects_to_delete
.append(GP_strokes_mesh
)
3929 GP_strokes_mesh
.data
.resolution_u
= 1
3930 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3932 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3933 self
.main_curve
.select_set(True)
3934 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3936 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3937 curves_duplicate_1
= bpy
.context
.object
3938 objects_to_delete
.append(curves_duplicate_1
)
3940 minimum_points_num
= 500
3942 # Some iterations since the subdivision operator
3943 # has a limit of 100 subdivisions per iteration
3944 for x
in range(round(minimum_points_num
/ 100)):
3945 # Check if the number of points of each curve has at least the number of points
3946 # of minimum_points_num. If not, subdivide to reach at least that number of points
3947 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3948 sp
= curves_duplicate_1
.data
.splines
[i
]
3950 if len(sp
.bezier_points
) < minimum_points_num
:
3951 for bp
in sp
.bezier_points
:
3952 bp
.select_control_point
= True
3954 if (len(sp
.bezier_points
) - 1) != 0:
3955 # Formula to get the number of cuts that will make a curve of N
3956 # number of points have near to "minimum_points_num" points,
3957 # when subdividing with this number of cuts
3958 subdivide_cuts
= int(
3959 (minimum_points_num
- len(sp
.bezier_points
)) /
3960 (len(sp
.bezier_points
) - 1)
3965 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3966 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3967 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3968 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3970 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3971 curves_duplicate_2
= bpy
.context
.object
3972 objects_to_delete
.append(curves_duplicate_2
)
3974 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3975 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3976 curves_duplicate_2
.select_set(True)
3977 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3979 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3980 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
3981 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
3982 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
='Shrinkwrap')
3984 # Get the distance of each vert from its original position to its position with Shrinkwrap
3985 nearest_points_coords
= {}
3986 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3987 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3988 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3989 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3991 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3992 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3995 shortest_dist
= (bp_1_co
- bp_2_co
).length
3996 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3997 "%.4f" % bp_2_co
[1],
3998 "%.4f" % bp_2_co
[2])
4000 dist
= (bp_1_co
- bp_2_co
).length
4002 if dist
< shortest_dist
:
4003 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4004 "%.4f" % bp_2_co
[1],
4005 "%.4f" % bp_2_co
[2])
4006 shortest_dist
= dist
4008 # Get all coords of GP strokes points, for comparison
4009 GP_strokes_coords
= []
4010 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
4011 GP_strokes_coords
.append(
4012 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4013 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4014 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4015 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4018 # Check the point of the GP strokes with the same coords as
4019 # the nearest points of the curves (with shrinkwrap)
4021 # Dictionary with GP stroke index as index, and a list as value.
4022 # The list has as index the point index of the GP stroke
4023 # nearest to the spline, and as value the spline index
4024 GP_connection_points
= {}
4025 for gp_st_idx
in range(len(GP_strokes_coords
)):
4026 GPvert_spline_relationship
= {}
4028 for splines_st_idx
in range(len(nearest_points_coords
)):
4029 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4030 GPvert_spline_relationship
[
4031 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4034 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4036 # Get the splines new order
4037 splines_new_order
= []
4038 for i
in GP_connection_points
:
4039 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4042 splines_new_order
.append(GP_connection_points
[i
][k
])
4045 curve_original_name
= self
.main_curve
.name
4047 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4048 self
.main_curve
.select_set(True)
4049 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4051 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4053 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4054 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4055 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4057 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4058 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4060 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4061 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4062 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4064 # Get the names of the separated splines objects in the original order
4065 splines_unordered
= {}
4066 for o
in bpy
.data
.objects
:
4067 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4068 spline_order_string
= o
.name
.partition(".")[2]
4070 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4071 spline_order_index
= int(spline_order_string
) - 1
4072 splines_unordered
[spline_order_index
] = o
.name
4074 # Join all splines objects in final order
4075 for order_idx
in splines_new_order
:
4076 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4077 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4078 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4079 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4081 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4083 # Go back to the original name of the curves object.
4084 bpy
.context
.object.name
= curve_original_name
4086 # Delete all unused objects
4087 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
4089 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4090 bpy
.data
.objects
[curve_original_name
].select_set(True)
4091 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4093 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4094 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4097 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4104 def invoke(self
, context
, event
):
4105 self
.main_curve
= bpy
.context
.object
4106 there_are_GP_strokes
= False
4109 # Get the active grease pencil layer
4110 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4113 there_are_GP_strokes
= True
4117 if there_are_GP_strokes
:
4118 self
.execute(context
)
4119 self
.report({'INFO'}, "Splines have been reordered")
4121 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4125 # ----------------------------
4126 # Set first points operator
4127 class CURVE_OT_SURFSK_first_points(Operator
):
4128 bl_idname
= "curve.surfsk_first_points"
4129 bl_label
= "Bsurfaces set first points"
4130 bl_description
= "Set the selected points as the first point of each spline"
4131 bl_options
= {'REGISTER', 'UNDO'}
4133 def execute(self
, context
):
4134 splines_to_invert
= []
4136 # Check non-cyclic splines to invert
4137 for i
in range(len(self
.main_curve
.data
.splines
)):
4138 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4140 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4141 if b_points
[len(b_points
) - 1].select_control_point
:
4142 splines_to_invert
.append(i
)
4144 # Reorder points of cyclic splines, and set all handles to "Automatic"
4146 # Check first selected point
4147 cyclic_splines_new_first_pt
= {}
4148 for i
in self
.cyclic_splines
:
4149 sp
= self
.main_curve
.data
.splines
[i
]
4151 for t
in range(len(sp
.bezier_points
)):
4152 bp
= sp
.bezier_points
[t
]
4153 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4154 cyclic_splines_new_first_pt
[i
] = t
4155 break # To take only one if there are more
4158 for spline_idx
in cyclic_splines_new_first_pt
:
4159 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4161 spline_old_coords
= []
4162 for bp_old
in sp
.bezier_points
:
4163 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4165 left_handle_type
= str(bp_old
.handle_left_type
)
4166 left_handle_length
= float(bp_old
.handle_left
.length
)
4168 float(bp_old
.handle_left
.x
),
4169 float(bp_old
.handle_left
.y
),
4170 float(bp_old
.handle_left
.z
)
4172 right_handle_type
= str(bp_old
.handle_right_type
)
4173 right_handle_length
= float(bp_old
.handle_right
.length
)
4174 right_handle_xyz
= (
4175 float(bp_old
.handle_right
.x
),
4176 float(bp_old
.handle_right
.y
),
4177 float(bp_old
.handle_right
.z
)
4179 spline_old_coords
.append(
4180 [coords
, left_handle_type
,
4181 right_handle_type
, left_handle_length
,
4182 right_handle_length
, left_handle_xyz
,
4186 for t
in range(len(sp
.bezier_points
)):
4187 bp
= sp
.bezier_points
4189 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4190 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4192 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4194 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4196 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4197 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4199 bp
[t
].handle_left_type
= "FREE"
4200 bp
[t
].handle_right_type
= "FREE"
4202 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4203 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4204 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4206 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4207 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4208 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4210 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4211 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4213 # Invert the non-cyclic splines designated above
4214 for i
in range(len(splines_to_invert
)):
4215 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4217 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4218 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4219 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4221 bpy
.ops
.curve
.switch_direction()
4223 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4225 # Keep selected the first vert of each spline
4226 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4227 for i
in range(len(self
.main_curve
.data
.splines
)):
4228 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4229 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4231 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4232 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4235 bp
.select_control_point
= True
4236 bp
.select_right_handle
= True
4237 bp
.select_left_handle
= True
4239 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4243 def invoke(self
, context
, event
):
4244 self
.main_curve
= bpy
.context
.object
4246 # Check if all curves are Bezier, and detect which ones are cyclic
4247 self
.cyclic_splines
= []
4248 for i
in range(len(self
.main_curve
.data
.splines
)):
4249 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4250 self
.report({'WARNING'}, "All splines must be Bezier type")
4252 return {'CANCELLED'}
4254 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4255 self
.cyclic_splines
.append(i
)
4257 self
.execute(context
)
4258 self
.report({'INFO'}, "First points have been set")
4263 # Add-ons Preferences Update Panel
4265 # Define Panel classes for updating
4267 VIEW3D_PT_tools_SURFSK_mesh
,
4268 VIEW3D_PT_tools_SURFSK_curve
4272 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4273 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4274 newCurve
.dimensions
= '3D'
4275 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4278 if type == 'GPensil':
4280 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4283 CurveObject
.location
= pencil
.location
4284 CurveObject
.rotation_euler
= pencil
.rotation_euler
4285 CurveObject
.scale
= pencil
.scale
4286 elif type == 'Annotation':
4288 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4291 CurveObject
.location
= (0.0, 0.0, 0.0)
4292 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4293 CurveObject
.scale
= (1.0, 1.0, 1.0)
4296 for i
, _stroke
in enumerate(strokes
):
4297 stroke_points
= strokes
[i
].points
4298 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4299 for point
in stroke_points
]
4300 points_to_add
= len(data_list
)-1
4303 for point
in data_list
:
4304 flat_list
.extend(point
)
4306 spline
= newCurve
.splines
.new(type='BEZIER')
4307 spline
.bezier_points
.add(points_to_add
)
4308 spline
.bezier_points
.foreach_set("co", flat_list
)
4310 for point
in spline
.bezier_points
:
4311 point
.handle_left_type
="AUTO"
4312 point
.handle_right_type
="AUTO"
4319 def update_panel(self
, context
):
4320 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4322 for panel
in panels
:
4323 if "bl_rna" in panel
.__dict
__:
4324 bpy
.utils
.unregister_class(panel
)
4326 for panel
in panels
:
4327 category
= context
.preferences
.addons
[__name__
].preferences
.category
4328 if category
!= 'Tool':
4329 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4331 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4332 panel
.bl_category
= 'Edit'
4333 raise ValueError("You can not install add-ons in the Tool panel")
4334 bpy
.utils
.register_class(panel
)
4336 except Exception as e
:
4337 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4340 def makeMaterial(name
, diffuse
):
4342 if name
in bpy
.data
.materials
:
4343 material
= bpy
.data
.materials
[name
]
4344 material
.diffuse_color
= diffuse
4346 material
= bpy
.data
.materials
.new(name
)
4347 material
.diffuse_color
= diffuse
4351 def update_mesh(self
, context
):
4353 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4354 bpy
.ops
.object.select_all(action
='DESELECT')
4355 bpy
.context
.view_layer
.update()
4356 global global_mesh_object
4357 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4358 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4359 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4361 print("Select mesh object")
4363 def update_gpencil(self
, context
):
4365 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4366 bpy
.ops
.object.select_all(action
='DESELECT')
4367 bpy
.context
.view_layer
.update()
4368 global global_gpencil_object
4369 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4370 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4371 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4373 print("Select gpencil object")
4375 def update_curve(self
, context
):
4377 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4378 bpy
.ops
.object.select_all(action
='DESELECT')
4379 bpy
.context
.view_layer
.update()
4380 global global_curve_object
4381 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4382 bpy
.data
.objects
[global_curve_object
].select_set(True)
4383 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4385 print("Select curve object")
4387 def update_shade_smooth(self
, context
):
4389 global global_shade_smooth
4390 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4392 contex_mode
= bpy
.context
.mode
4394 if bpy
.ops
.object.mode_set
.poll():
4395 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4397 bpy
.ops
.object.select_all(action
='DESELECT')
4398 global global_mesh_object
4399 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4400 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4402 if global_shade_smooth
:
4403 bpy
.ops
.object.shade_smooth()
4405 bpy
.ops
.object.shade_flat()
4407 if contex_mode
== "EDIT_MESH":
4408 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4411 print("Select mesh object")
4414 class BsurfPreferences(AddonPreferences
):
4415 # this must match the addon name, use '__package__'
4416 # when defining this in a submodule of a python package.
4417 bl_idname
= __name__
4419 category
: StringProperty(
4420 name
="Tab Category",
4421 description
="Choose a name for the category of the panel",
4426 def draw(self
, context
):
4427 layout
= self
.layout
4431 col
.label(text
="Tab Category:")
4432 col
.prop(self
, "category", text
="")
4435 class BsurfacesProps(PropertyGroup
):
4436 SURFSK_guide
: EnumProperty(
4439 ('Annotation', 'Annotation', 'Annotation'),
4440 ('GPencil', 'GPencil', 'GPencil'),
4441 ('Curve', 'Curve', 'Curve')
4443 default
="Annotation"
4445 SURFSK_edges_U
: IntProperty(
4447 description
="Number of face-loops crossing the strokes",
4452 SURFSK_edges_V
: IntProperty(
4454 description
="Number of face-loops following the strokes",
4459 SURFSK_cyclic_cross
: BoolProperty(
4460 name
="Cyclic Cross",
4461 description
="Make cyclic the face-loops crossing the strokes",
4464 SURFSK_cyclic_follow
: BoolProperty(
4465 name
="Cyclic Follow",
4466 description
="Make cyclic the face-loops following the strokes",
4469 SURFSK_keep_strokes
: BoolProperty(
4470 name
="Keep strokes",
4471 description
="Keeps the sketched strokes or curves after adding the surface",
4474 SURFSK_automatic_join
: BoolProperty(
4475 name
="Automatic join",
4476 description
="Join automatically vertices of either surfaces "
4477 "generated by crosshatching, or from the borders of closed shapes",
4480 SURFSK_loops_on_strokes
: BoolProperty(
4481 name
="Loops on strokes",
4482 description
="Make the loops match the paths of the strokes",
4485 SURFSK_precision
: IntProperty(
4487 description
="Precision level of the surface calculation",
4492 SURFSK_mesh
: PointerProperty(
4493 name
="Mesh of BSurface",
4494 type=bpy
.types
.Object
,
4495 description
="Mesh of BSurface",
4498 SURFSK_gpencil
: PointerProperty(
4499 name
="GreasePencil object",
4500 type=bpy
.types
.Object
,
4501 description
="GreasePencil object",
4502 update
=update_gpencil
,
4504 SURFSK_curve
: PointerProperty(
4505 name
="Curve object",
4506 type=bpy
.types
.Object
,
4507 description
="Curve object",
4508 update
=update_curve
,
4510 SURFSK_shade_smooth
: BoolProperty(
4511 name
="Shade smooth",
4512 description
="Render and display faces smooth, using interpolated Vertex Normals",
4514 update
=update_shade_smooth
,
4518 MESH_OT_SURFSK_init
,
4519 MESH_OT_SURFSK_add_modifiers
,
4520 MESH_OT_SURFSK_add_surface
,
4521 MESH_OT_SURFSK_edit_surface
,
4522 GPENCIL_OT_SURFSK_add_strokes
,
4523 GPENCIL_OT_SURFSK_edit_strokes
,
4524 GPENCIL_OT_SURFSK_strokes_to_curves
,
4525 GPENCIL_OT_SURFSK_annotation_to_curves
,
4526 GPENCIL_OT_SURFSK_add_annotation
,
4527 CURVE_OT_SURFSK_edit_curve
,
4528 CURVE_OT_SURFSK_reorder_splines
,
4529 CURVE_OT_SURFSK_first_points
,
4536 bpy
.utils
.register_class(cls
)
4538 for panel
in panels
:
4539 bpy
.utils
.register_class(panel
)
4541 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4542 update_panel(None, bpy
.context
)
4545 for panel
in panels
:
4546 bpy
.utils
.unregister_class(panel
)
4549 bpy
.utils
.unregister_class(cls
)
4551 del bpy
.types
.Scene
.bsurfaces
4553 if __name__
== "__main__":