1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "name": "Bsurfaces GPL Edition",
6 "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
9 "location": "View3D EditMode > Sidebar > Edit Tab",
10 "description": "Modeling and retopology tool",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
18 from bpy_extras
import object_utils
21 from mathutils
import Matrix
, Vector
22 from mathutils
.geometry
import (
31 from bpy
.props
import (
40 from bpy
.types
import (
47 # ----------------------------
49 global_shade_smooth
= False
50 global_mesh_object
= ""
51 global_gpencil_object
= ""
52 global_curve_object
= ""
54 # ----------------------------
56 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
57 bl_space_type
= 'VIEW_3D'
60 bl_label
= "Bsurfaces"
62 def draw(self
, context
):
64 bs
= context
.scene
.bsurfaces
66 col
= layout
.column(align
=True)
69 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
70 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
72 col
.label(text
="Mesh of BSurface:")
73 col
.prop(bs
, "SURFSK_mesh", text
="")
74 if bs
.SURFSK_mesh
!= None:
75 try: mesh_object
= bs
.SURFSK_mesh
77 try: col
.prop(mesh_object
.data
.materials
[0], "diffuse_color")
79 try: col
.prop(mesh_object
.modifiers
['Shrinkwrap'], "offset")
81 try: col
.prop(mesh_object
, "show_in_front")
83 try: col
.prop(bs
, "SURFSK_shade_smooth")
85 try: col
.prop(mesh_object
, "show_wire")
88 col
.label(text
="Guide strokes:")
89 col
.row().prop(bs
, "SURFSK_guide", expand
=True)
90 if bs
.SURFSK_guide
== 'GPencil':
91 col
.prop(bs
, "SURFSK_gpencil", text
="")
93 if bs
.SURFSK_guide
== 'Curve':
94 col
.prop(bs
, "SURFSK_curve", text
="")
98 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
99 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
102 if bs
.SURFSK_guide
== 'GPencil':
103 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
104 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
106 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
108 if bs
.SURFSK_guide
== 'Annotation':
109 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
111 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
113 if bs
.SURFSK_guide
== 'Curve':
114 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
117 col
.label(text
="Initial settings:")
118 col
.prop(bs
, "SURFSK_edges_U")
119 col
.prop(bs
, "SURFSK_edges_V")
120 col
.prop(bs
, "SURFSK_cyclic_cross")
121 col
.prop(bs
, "SURFSK_cyclic_follow")
122 col
.prop(bs
, "SURFSK_loops_on_strokes")
123 col
.prop(bs
, "SURFSK_automatic_join")
124 col
.prop(bs
, "SURFSK_keep_strokes")
126 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
127 bl_space_type
= 'VIEW_3D'
128 bl_region_type
= 'UI'
129 bl_context
= "curve_edit"
131 bl_label
= "Bsurfaces"
134 def poll(cls
, context
):
135 return context
.active_object
137 def draw(self
, context
):
140 col
= layout
.column(align
=True)
143 col
.operator("curve.surfsk_first_points", text
="Set First Points")
144 col
.operator("curve.switch_direction", text
="Switch Direction")
145 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
148 # ----------------------------
149 # Returns the type of strokes used
150 def get_strokes_type(context
):
151 strokes_type
= "NO_STROKES"
154 # Check if they are annotation
155 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
157 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
159 strokes_num
= len(strokes
)
162 strokes_type
= "GP_ANNOTATION"
164 strokes_type
= "NO_STROKES"
166 # Check if they are grease pencil
167 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
169 global global_gpencil_object
170 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
171 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
173 strokes_num
= len(strokes
)
176 strokes_type
= "GP_STROKES"
178 strokes_type
= "NO_STROKES"
180 # Check if they are curves, if there aren't grease pencil strokes
181 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
183 global global_curve_object
184 ob
= bpy
.data
.objects
[global_curve_object
]
185 if ob
.type == "CURVE":
186 strokes_type
= "EXTERNAL_CURVE"
187 strokes_num
= len(ob
.data
.splines
)
189 # Check if there is any non-bezier spline
190 for i
in range(len(ob
.data
.splines
)):
191 if ob
.data
.splines
[i
].type != "BEZIER":
192 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
196 strokes_type
= "EXTERNAL_NO_CURVE"
198 strokes_type
= "NO_STROKES"
200 # Check if they are mesh
202 global global_mesh_object
203 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
204 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
206 # Check if there is a single stroke without any selection in the object
207 if strokes_num
== 1 and total_vert_sel
== 0:
208 if strokes_type
== "EXTERNAL_CURVE":
209 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
210 elif strokes_type
== "GP_STROKES":
211 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
213 if strokes_num
== 0 and total_vert_sel
> 0:
214 strokes_type
= "SELECTION_ALONE"
220 # ----------------------------
221 # Surface generator operator
222 class MESH_OT_SURFSK_add_surface(Operator
):
223 bl_idname
= "mesh.surfsk_add_surface"
224 bl_label
= "Bsurfaces add surface"
225 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
226 bl_options
= {'REGISTER', 'UNDO'}
228 is_crosshatch
: BoolProperty(
231 is_fill_faces
: BoolProperty(
234 selection_U_exists
: BoolProperty(
237 selection_V_exists
: BoolProperty(
240 selection_U2_exists
: BoolProperty(
243 selection_V2_exists
: BoolProperty(
246 selection_V_is_closed
: BoolProperty(
249 selection_U_is_closed
: BoolProperty(
252 selection_V2_is_closed
: BoolProperty(
255 selection_U2_is_closed
: BoolProperty(
259 edges_U
: IntProperty(
261 description
="Number of face-loops crossing the strokes",
266 edges_V
: IntProperty(
268 description
="Number of face-loops following the strokes",
273 cyclic_cross
: BoolProperty(
275 description
="Make cyclic the face-loops crossing the strokes",
278 cyclic_follow
: BoolProperty(
279 name
="Cyclic Follow",
280 description
="Make cyclic the face-loops following the strokes",
283 loops_on_strokes
: BoolProperty(
284 name
="Loops on strokes",
285 description
="Make the loops match the paths of the strokes",
288 automatic_join
: BoolProperty(
289 name
="Automatic join",
290 description
="Join automatically vertices of either surfaces generated "
291 "by crosshatching, or from the borders of closed shapes",
294 join_stretch_factor
: FloatProperty(
296 description
="Amount of stretching or shrinking allowed for "
297 "edges when joining vertices automatically",
303 keep_strokes
: BoolProperty(
305 description
="Keeps the sketched strokes or curves after adding the surface",
308 strokes_type
: StringProperty()
309 initial_global_undo_state
: BoolProperty()
312 def draw(self
, context
):
314 col
= layout
.column(align
=True)
317 if not self
.is_fill_faces
:
319 if not self
.is_crosshatch
:
320 if not self
.selection_U_exists
:
321 col
.prop(self
, "edges_U")
324 if not self
.selection_V_exists
:
325 col
.prop(self
, "edges_V")
330 if not self
.selection_U_exists
:
332 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
333 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
335 col
.prop(self
, "cyclic_cross")
337 if not self
.selection_V_exists
:
339 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
340 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
342 col
.prop(self
, "cyclic_follow")
344 col
.prop(self
, "loops_on_strokes")
346 col
.prop(self
, "automatic_join")
348 if self
.automatic_join
:
352 col
.prop(self
, "join_stretch_factor")
354 col
.prop(self
, "keep_strokes")
356 # Get an ordered list of a chain of vertices
357 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
358 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
359 # Order selected vertices.
361 if closing_vert_idx
is not None:
362 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
364 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
365 prev_v
= first_vert_idx
369 edges_non_matched
= 0
370 for i
in all_selected_edges_idx
:
371 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
372 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
374 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
375 prev_v
= ob
.data
.edges
[i
].vertices
[1]
376 prev_ed
= ob
.data
.edges
[i
]
377 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
378 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
380 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
381 prev_v
= ob
.data
.edges
[i
].vertices
[0]
382 prev_ed
= ob
.data
.edges
[i
]
384 edges_non_matched
+= 1
386 if edges_non_matched
== len(all_selected_edges_idx
):
392 if closing_vert_idx
is not None:
393 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
395 if middle_vertex_idx
is not None:
396 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
397 verts_ordered
.reverse()
399 return tuple(verts_ordered
)
401 # Calculates length of a chain of points.
402 def get_chain_length(self
, object, verts_ordered
):
403 matrix
= object.matrix_world
406 edges_lengths_sum
= 0
407 for i
in range(0, len(verts_ordered
)):
409 prev_v_co
= matrix
@ verts_ordered
[i
].co
411 v_co
= matrix
@ verts_ordered
[i
].co
413 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
414 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
416 edges_lengths
.append(edge_length
)
417 edges_lengths_sum
+= edge_length
421 return edges_lengths
, edges_lengths_sum
423 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
424 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
425 edges_proportions
= []
428 for l
in edges_lengths
:
429 edges_proportions
.append(l
/ edges_lengths_sum
)
433 for _n
in range(0, fixed_edges_num
):
434 edges_proportions
.append(1 / fixed_edges_num
)
437 return edges_proportions
439 # Calculates the angle between two pairs of points in space
440 def orientation_difference(self
, points_A_co
, points_B_co
):
441 # each parameter should be a list with two elements,
442 # and each element should be a x,y,z coordinate
443 vec_A
= points_A_co
[0] - points_A_co
[1]
444 vec_B
= points_B_co
[0] - points_B_co
[1]
446 angle
= vec_A
.angle(vec_B
)
449 angle
= abs(angle
- pi
)
453 # Calculate the which vert of verts_idx list is the nearest one
454 # to the point_co coordinates, and the distance
455 def shortest_distance(self
, object, point_co
, verts_idx
):
456 matrix
= object.matrix_world
458 for i
in range(0, len(verts_idx
)):
459 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
462 nearest_vert_idx
= verts_idx
[i
]
467 nearest_vert_idx
= verts_idx
[i
]
470 return nearest_vert_idx
, shortest_dist
472 # Returns the index of the opposite vert tip in a chain, given a vert tip index
473 # as parameter, and a multidimentional list with all pairs of tips
474 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
475 opposite_vert_tip_idx
= None
476 for i
in range(0, len(all_chains_tips_idx
)):
477 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
478 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
479 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
480 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
482 return opposite_vert_tip_idx
484 # Simplifies a spline and returns the new points coordinates
485 def simplify_spline(self
, spline_coords
, segments_num
):
486 simplified_spline
= []
487 points_between_segments
= round(len(spline_coords
) / segments_num
)
489 simplified_spline
.append(spline_coords
[0])
490 for i
in range(1, segments_num
):
491 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
493 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
495 return simplified_spline
497 # Returns a list with the coords of the points distributed over the splines
498 # passed to this method according to the proportions parameter
499 def distribute_pts(self
, surface_splines
, proportions
):
501 # Calculate the length of each final surface spline
502 surface_splines_lengths
= []
503 surface_splines_parsed
= []
505 for sp_idx
in range(0, len(surface_splines
)):
506 # Calculate spline length
507 surface_splines_lengths
.append(0)
509 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
511 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
513 p
= surface_splines
[sp_idx
].bezier_points
[i
]
514 edge_length
= (prev_p
.co
- p
.co
).length
515 surface_splines_lengths
[sp_idx
] += edge_length
519 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
520 for sp_idx
in range(0, len(surface_splines
)):
521 surface_splines_parsed
.append([])
522 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
524 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
527 for prop_idx
in range(len(proportions
) - 1):
528 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
529 partial_segment_length
= 0
533 # if not it'll pass the p_idx as an index below and crash
534 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
535 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
536 new_dist
= (prev_p_co
- p_co
).length
538 # The new distance that could have the partial segment if
539 # it is still shorter than the target length
540 potential_segment_length
= partial_segment_length
+ new_dist
542 # If the potential is still shorter, keep adding
543 if potential_segment_length
< target_length
:
544 partial_segment_length
= potential_segment_length
549 # If the potential is longer than the target, calculate the target
550 # (a point between the last two points), and assign
551 elif potential_segment_length
> target_length
:
552 remaining_dist
= target_length
- partial_segment_length
553 vec
= p_co
- prev_p_co
555 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
557 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
559 partial_segment_length
+= remaining_dist
560 prev_p_co
= intermediate_co
564 # If the potential is equal to the target, assign
565 elif potential_segment_length
== target_length
:
566 surface_splines_parsed
[sp_idx
].append(p_co
)
574 # last point of the spline
575 surface_splines_parsed
[sp_idx
].append(
576 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
579 return surface_splines_parsed
581 # Counts the number of faces that belong to each edge
582 def edge_face_count(self
, ob
):
583 ed_keys_count_dict
= {}
585 for face
in ob
.data
.polygons
:
586 for ed_keys
in face
.edge_keys
:
587 if ed_keys
not in ed_keys_count_dict
:
588 ed_keys_count_dict
[ed_keys
] = 1
590 ed_keys_count_dict
[ed_keys
] += 1
593 for i
in range(len(ob
.data
.edges
)):
594 edge_face_count
.append(0)
596 for i
in range(len(ob
.data
.edges
)):
597 ed
= ob
.data
.edges
[i
]
602 if (v1
, v2
) in ed_keys_count_dict
:
603 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
604 elif (v2
, v1
) in ed_keys_count_dict
:
605 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
607 return edge_face_count
609 # Fills with faces all the selected vertices which form empty triangles or quads
610 def fill_with_faces(self
, object):
611 all_selected_verts_count
= self
.main_object_selected_verts_count
613 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
615 # Calculate average length of selected edges
616 all_selected_verts
= []
617 original_sel_edges_count
= 0
618 for ed
in object.data
.edges
:
619 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
621 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
622 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
624 original_sel_edges_count
+= 1
626 if not ed
.vertices
[0] in all_selected_verts
:
627 all_selected_verts
.append(ed
.vertices
[0])
629 if not ed
.vertices
[1] in all_selected_verts
:
630 all_selected_verts
.append(ed
.vertices
[1])
632 tuple(all_selected_verts
)
634 # Check if there is any edge selected. If not, interrupt the script
635 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
638 # Get all edges connected to selected verts
639 all_edges_around_sel_verts
= []
640 edges_connected_to_sel_verts
= {}
641 verts_connected_to_every_vert
= {}
642 for ed_idx
in range(len(object.data
.edges
)):
643 ed
= object.data
.edges
[ed_idx
]
646 if ed
.vertices
[0] in all_selected_verts
:
647 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
648 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
650 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
653 if ed
.vertices
[1] in all_selected_verts
:
654 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
655 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
657 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
660 if include_edge
is True:
661 all_edges_around_sel_verts
.append(ed_idx
)
663 # Get all connected verts to each vert
664 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
665 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
667 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
668 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
670 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
671 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
673 # Get all verts connected to faces
674 all_verts_part_of_faces
= []
675 all_edges_faces_count
= []
676 all_edges_faces_count
+= self
.edge_face_count(object)
678 # Get only the selected edges that have faces attached.
679 count_faces_of_edges_around_sel_verts
= {}
680 selected_verts_with_faces
= []
681 for ed_idx
in all_edges_around_sel_verts
:
682 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
684 if all_edges_faces_count
[ed_idx
] > 0:
685 ed
= object.data
.edges
[ed_idx
]
687 if not ed
.vertices
[0] in selected_verts_with_faces
:
688 selected_verts_with_faces
.append(ed
.vertices
[0])
690 if not ed
.vertices
[1] in selected_verts_with_faces
:
691 selected_verts_with_faces
.append(ed
.vertices
[1])
693 all_verts_part_of_faces
.append(ed
.vertices
[0])
694 all_verts_part_of_faces
.append(ed
.vertices
[1])
696 tuple(selected_verts_with_faces
)
698 # Discard unneeded verts from calculations
699 participating_verts
= []
701 for v_idx
in all_selected_verts
:
702 vert_has_edges_with_one_face
= False
704 # Check if the actual vert has at least one edge connected to only one face
705 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
706 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
707 vert_has_edges_with_one_face
= True
709 # If the vert has two or less edges connected and the vert is not part of any face.
710 # Or the vert is part of any face and at least one of
711 # the connected edges has only one face attached to it.
712 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
713 v_idx
not in all_verts_part_of_faces
) or \
714 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
715 (v_idx
in all_verts_part_of_faces
and
716 vert_has_edges_with_one_face
):
718 participating_verts
.append(v_idx
)
720 if v_idx
not in all_verts_part_of_faces
:
721 movable_verts
.append(v_idx
)
723 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
724 for mv_idx
in movable_verts
:
726 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
728 for actual_v_idx
in all_selected_verts
:
729 count_shared_neighbors
= 0
732 for mv_conn_v_idx
in mv_connected_verts
:
733 if mv_idx
!= actual_v_idx
:
734 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
735 mv_conn_v_idx
not in checked_verts
:
736 count_shared_neighbors
+= 1
737 checked_verts
.append(mv_conn_v_idx
)
739 if actual_v_idx
in mv_connected_verts
:
743 if count_shared_neighbors
== 2:
751 movable_verts
.remove(mv_idx
)
753 # Calculate merge distance for participating verts
754 shortest_edge_length
= None
755 for ed
in object.data
.edges
:
756 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
757 v1
= object.data
.vertices
[ed
.vertices
[0]]
758 v2
= object.data
.vertices
[ed
.vertices
[1]]
760 length
= (v1
.co
- v2
.co
).length
762 if shortest_edge_length
is None:
763 shortest_edge_length
= length
765 if length
< shortest_edge_length
:
766 shortest_edge_length
= length
768 if shortest_edge_length
is not None:
769 edges_merge_distance
= shortest_edge_length
* 0.5
771 edges_merge_distance
= 0
773 # Get together the verts near enough. They will be merged later
775 remaining_verts
+= participating_verts
776 for v1_idx
in participating_verts
:
777 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
779 coords_verts_to_merge
= {}
781 verts_to_merge
.append(v1_idx
)
783 v1_co
= object.data
.vertices
[v1_idx
].co
784 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
786 for v2_idx
in remaining_verts
:
788 v2_co
= object.data
.vertices
[v2_idx
].co
790 dist
= (v1_co
- v2_co
).length
792 if dist
<= edges_merge_distance
: # Add the verts which are near enough
793 verts_to_merge
.append(v2_idx
)
795 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
797 for vm_idx
in verts_to_merge
:
798 remaining_verts
.remove(vm_idx
)
800 if len(verts_to_merge
) > 1:
801 # Calculate middle point of the verts to merge.
805 movable_verts_to_merge_count
= 0
806 for i
in range(len(verts_to_merge
)):
807 if verts_to_merge
[i
] in movable_verts
:
808 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
814 movable_verts_to_merge_count
+= 1
817 sum_x_co
/ movable_verts_to_merge_count
,
818 sum_y_co
/ movable_verts_to_merge_count
,
819 sum_z_co
/ movable_verts_to_merge_count
822 # Check if any vert to be merged is not movable
824 are_verts_not_movable
= False
825 verts_not_movable
= []
826 for v_merge_idx
in verts_to_merge
:
827 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
828 are_verts_not_movable
= True
829 verts_not_movable
.append(v_merge_idx
)
831 if are_verts_not_movable
:
832 # Get the vert connected to faces, that is nearest to
833 # the middle point of the movable verts
835 for vcf_idx
in verts_not_movable
:
836 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
837 Vector(middle_point_co
)).length
)
839 if shortest_dist
is None:
841 nearest_vert_idx
= vcf_idx
843 if dist
< shortest_dist
:
845 nearest_vert_idx
= vcf_idx
847 coords
= object.data
.vertices
[nearest_vert_idx
].co
848 target_point_co
= [coords
[0], coords
[1], coords
[2]]
850 target_point_co
= middle_point_co
852 # Move verts to merge to the middle position
853 for v_merge_idx
in verts_to_merge
:
854 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
855 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
856 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
857 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
859 # Perform "Remove Doubles" to weld all the disconnected verts
860 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
861 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
863 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
865 # Get all the definitive selected edges, after weldding
867 edges_per_vert
= {} # Number of faces of each selected edge
868 for ed
in object.data
.edges
:
869 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
870 selected_edges
.append(ed
.index
)
872 # Save all the edges that belong to each vertex.
873 if not ed
.vertices
[0] in edges_per_vert
:
874 edges_per_vert
[ed
.vertices
[0]] = []
876 if not ed
.vertices
[1] in edges_per_vert
:
877 edges_per_vert
[ed
.vertices
[1]] = []
879 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
880 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
882 # Check if all the edges connected to each vert have two faces attached to them.
883 # To discard them later and make calculations faster
885 a
+= self
.edge_face_count(object)
887 verts_surrounded_by_faces
= {}
888 for v_idx
in edges_per_vert
:
889 edges_with_two_faces_count
= 0
891 for ed_idx
in edges_per_vert
[v_idx
]:
893 edges_with_two_faces_count
+= 1
895 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
896 verts_surrounded_by_faces
[v_idx
] = True
898 verts_surrounded_by_faces
[v_idx
] = False
900 # Get all the selected vertices
901 selected_verts_idx
= []
902 for v
in object.data
.vertices
:
904 selected_verts_idx
.append(v
.index
)
906 # Get all the faces of the object
907 all_object_faces_verts_idx
= []
908 for face
in object.data
.polygons
:
910 face_verts
.append(face
.vertices
[0])
911 face_verts
.append(face
.vertices
[1])
912 face_verts
.append(face
.vertices
[2])
914 if len(face
.vertices
) == 4:
915 face_verts
.append(face
.vertices
[3])
917 all_object_faces_verts_idx
.append(face_verts
)
919 # Deselect all vertices
920 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
921 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
922 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
924 # Make a dictionary with the verts related to each vert
925 related_key_verts
= {}
926 for ed_idx
in selected_edges
:
927 ed
= object.data
.edges
[ed_idx
]
929 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
930 if not ed
.vertices
[0] in related_key_verts
:
931 related_key_verts
[ed
.vertices
[0]] = []
933 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
934 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
936 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
937 if not ed
.vertices
[1] in related_key_verts
:
938 related_key_verts
[ed
.vertices
[1]] = []
940 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
941 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
943 # Get groups of verts forming each face
945 for v1
in related_key_verts
: # verts-1 ....
946 for v2
in related_key_verts
: # verts-2
948 related_verts_in_common
= []
951 for rel_v1
in related_key_verts
[v1
]:
952 # Check if related verts of verts-1 are related verts of verts-2
953 if rel_v1
in related_key_verts
[v2
]:
954 related_verts_in_common
.append(rel_v1
)
956 if v2
in related_key_verts
[v1
]:
959 if v1
in related_key_verts
[v2
]:
962 repeated_face
= False
963 # If two verts have two related verts in common, they form a quad
964 if len(related_verts_in_common
) == 2:
965 # Check if the face is already saved
966 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
968 for f_verts
in all_faces_to_check_idx
:
971 if len(f_verts
) == 4:
976 if related_verts_in_common
[0] in f_verts
:
978 if related_verts_in_common
[1] in f_verts
:
981 if repeated_verts
== len(f_verts
):
985 if not repeated_face
:
986 faces_verts_idx
.append(
987 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
990 # If Two verts have one related vert in common and
991 # they are related to each other, they form a triangle
992 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
993 # Check if the face is already saved.
994 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
996 for f_verts
in all_faces_to_check_idx
:
999 if len(f_verts
) == 3:
1004 if related_verts_in_common
[0] in f_verts
:
1007 if repeated_verts
== len(f_verts
):
1008 repeated_face
= True
1011 if not repeated_face
:
1012 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1014 # Keep only the faces that don't overlap by ignoring quads
1015 # that overlap with two adjacent triangles
1016 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1017 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1018 for i
in range(len(faces_verts_idx
)):
1019 for t
in range(len(all_faces_to_check_idx
)):
1023 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1024 for v_idx
in all_faces_to_check_idx
[t
]:
1025 if v_idx
in faces_verts_idx
[i
]:
1026 verts_in_common
+= 1
1027 # If it doesn't have all it's vertices repeated in the other face
1028 if verts_in_common
== 3:
1029 if i
not in faces_to_not_include_idx
:
1030 faces_to_not_include_idx
.append(i
)
1032 # Build faces discarding the ones in faces_to_not_include
1037 num_faces_created
= 0
1038 for i
in range(len(faces_verts_idx
)):
1039 if i
not in faces_to_not_include_idx
:
1040 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1042 num_faces_created
+= 1
1047 for v_idx
in selected_verts_idx
:
1048 self
.main_object
.data
.vertices
[v_idx
].select
= True
1050 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1051 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1052 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1056 return num_faces_created
1058 # Crosshatch skinning
1059 def crosshatch_surface_invoke(self
, ob_original_splines
):
1060 self
.is_crosshatch
= False
1061 self
.crosshatch_merge_distance
= 0
1063 objects_to_delete
= [] # duplicated strokes to be deleted.
1065 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1066 # (without this the surface verts merging with the main object doesn't work well)
1067 self
.modifiers_prev_viewport_state
= []
1068 if len(self
.main_object
.modifiers
) > 0:
1069 for m_idx
in range(len(self
.main_object
.modifiers
)):
1070 self
.modifiers_prev_viewport_state
.append(
1071 self
.main_object
.modifiers
[m_idx
].show_viewport
1073 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1075 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1076 ob_original_splines
.select_set(True)
1077 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1079 if len(ob_original_splines
.data
.splines
) >= 2:
1080 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1081 ob_splines
= bpy
.context
.object
1082 ob_splines
.name
= "SURFSKIO_NE_STR"
1084 # Get estimative merge distance (sum up the distances from the first point to
1085 # all other points, then average them and then divide them)
1086 first_point_dist_sum
= 0
1089 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1090 for i
in range(len(ob_splines
.data
.splines
)):
1091 sp
= ob_splines
.data
.splines
[i
]
1093 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1094 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1096 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1097 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1099 first_point_dist_sum
+= first_dist
+ second_dist
1103 shortest_dist
= first_dist
1104 elif second_dist
!= 0:
1105 shortest_dist
= second_dist
1107 if shortest_dist
> first_dist
and first_dist
!= 0:
1108 shortest_dist
= first_dist
1110 if shortest_dist
> second_dist
and second_dist
!= 0:
1111 shortest_dist
= second_dist
1113 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1115 # Recalculation of merge distance
1117 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1119 ob_calc_merge_dist
= bpy
.context
.object
1120 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1122 objects_to_delete
.append(ob_calc_merge_dist
)
1124 # Smooth out strokes a little to improve crosshatch detection
1125 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1126 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1129 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1131 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1132 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1134 # Convert curves into mesh
1135 ob_calc_merge_dist
.data
.resolution_u
= 12
1136 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1138 # Find "intersection-nodes"
1139 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1140 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1141 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1142 threshold
=self
.crosshatch_merge_distance
)
1143 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1144 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1146 # Remove verts with less than three edges
1147 verts_edges_count
= {}
1148 for ed
in ob_calc_merge_dist
.data
.edges
:
1151 if v
[0] not in verts_edges_count
:
1152 verts_edges_count
[v
[0]] = 0
1154 if v
[1] not in verts_edges_count
:
1155 verts_edges_count
[v
[1]] = 0
1157 verts_edges_count
[v
[0]] += 1
1158 verts_edges_count
[v
[1]] += 1
1160 nodes_verts_coords
= []
1161 for v_idx
in verts_edges_count
:
1162 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1164 if verts_edges_count
[v_idx
] < 3:
1168 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1169 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1170 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1172 # Remove doubles to discard very near verts from calculations of distance
1173 bpy
.ops
.mesh
.remove_doubles(
1174 'INVOKE_REGION_WIN',
1175 threshold
=self
.crosshatch_merge_distance
* 4.0
1177 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1179 # Get all coords of the resulting nodes
1180 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1181 v
in ob_calc_merge_dist
.data
.vertices
]
1183 # Check if the strokes are a crosshatch
1184 if len(nodes_verts_coords
) >= 3:
1185 self
.is_crosshatch
= True
1187 shortest_dist
= None
1188 for co_1
in nodes_verts_coords
:
1189 for co_2
in nodes_verts_coords
:
1191 dist
= (Vector(co_1
) - Vector(co_2
)).length
1193 if shortest_dist
is not None:
1194 if dist
< shortest_dist
:
1195 shortest_dist
= dist
1197 shortest_dist
= dist
1199 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1201 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1202 ob_splines
.select_set(True)
1203 bpy
.context
.view_layer
.objects
.active
= ob_splines
1205 # Deselect all points
1206 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1207 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1208 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1210 # Smooth splines in a localized way, to eliminate "saw-teeth"
1211 # like shapes when there are many points
1212 for sp
in ob_splines
.data
.splines
:
1215 angle_limit
= 2 # Degrees
1216 for t
in range(len(sp
.bezier_points
)):
1217 # Because on each iteration it checks the "next two points"
1218 # of the actual. This way it doesn't go out of range
1219 if t
<= len(sp
.bezier_points
) - 3:
1220 p1
= sp
.bezier_points
[t
]
1221 p2
= sp
.bezier_points
[t
+ 1]
1222 p3
= sp
.bezier_points
[t
+ 2]
1224 vec_1
= p1
.co
- p2
.co
1225 vec_2
= p2
.co
- p3
.co
1227 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1228 angle
= vec_1
.angle(vec_2
)
1229 angle_sum
+= degrees(angle
)
1231 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1232 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1233 p1
.select_control_point
= True
1234 p1
.select_left_handle
= True
1235 p1
.select_right_handle
= True
1237 p2
.select_control_point
= True
1238 p2
.select_left_handle
= True
1239 p2
.select_right_handle
= True
1241 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1242 p3
.select_control_point
= True
1243 p3
.select_left_handle
= True
1244 p3
.select_right_handle
= True
1248 sp
.bezier_points
[0].select_control_point
= False
1249 sp
.bezier_points
[0].select_left_handle
= False
1250 sp
.bezier_points
[0].select_right_handle
= False
1252 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1253 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1254 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1256 # Smooth out strokes a little to improve crosshatch detection
1257 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1260 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1262 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1263 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1265 # Simplify the splines
1266 for sp
in ob_splines
.data
.splines
:
1269 sp
.bezier_points
[0].select_control_point
= True
1270 sp
.bezier_points
[0].select_left_handle
= True
1271 sp
.bezier_points
[0].select_right_handle
= True
1273 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1274 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1275 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1277 angle_limit
= 15 # Degrees
1278 for t
in range(len(sp
.bezier_points
)):
1279 # Because on each iteration it checks the "next two points"
1280 # of the actual. This way it doesn't go out of range
1281 if t
<= len(sp
.bezier_points
) - 3:
1282 p1
= sp
.bezier_points
[t
]
1283 p2
= sp
.bezier_points
[t
+ 1]
1284 p3
= sp
.bezier_points
[t
+ 2]
1286 vec_1
= p1
.co
- p2
.co
1287 vec_2
= p2
.co
- p3
.co
1289 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1290 angle
= vec_1
.angle(vec_2
)
1291 angle_sum
+= degrees(angle
)
1292 # If sum of angles is grater than the limit
1293 if angle_sum
>= angle_limit
:
1294 p1
.select_control_point
= True
1295 p1
.select_left_handle
= True
1296 p1
.select_right_handle
= True
1298 p2
.select_control_point
= True
1299 p2
.select_left_handle
= True
1300 p2
.select_right_handle
= True
1302 p3
.select_control_point
= True
1303 p3
.select_left_handle
= True
1304 p3
.select_right_handle
= True
1308 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1309 bpy
.ops
.curve
.select_all(action
='INVERT')
1311 bpy
.ops
.curve
.delete(type='VERT')
1312 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1314 objects_to_delete
.append(ob_splines
)
1316 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1317 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1318 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1320 # Check if the strokes are a crosshatch
1321 if self
.is_crosshatch
:
1322 all_points_coords
= []
1323 for i
in range(len(ob_splines
.data
.splines
)):
1324 all_points_coords
.append([])
1326 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1327 x
, y
, z
in [bp
.co
for
1328 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1330 all_intersections
= []
1331 checked_splines
= []
1332 for i
in range(len(all_points_coords
)):
1334 for t
in range(len(all_points_coords
[i
]) - 1):
1335 bp1_co
= all_points_coords
[i
][t
]
1336 bp2_co
= all_points_coords
[i
][t
+ 1]
1338 for i2
in range(len(all_points_coords
)):
1339 if i
!= i2
and i2
not in checked_splines
:
1340 for t2
in range(len(all_points_coords
[i2
]) - 1):
1341 bp3_co
= all_points_coords
[i2
][t2
]
1342 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1344 intersec_coords
= intersect_line_line(
1345 bp1_co
, bp2_co
, bp3_co
, bp4_co
1347 if intersec_coords
is not None:
1348 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1350 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1351 _temp_co
, percent1
= intersect_point_line(
1352 intersec_coords
[0], bp1_co
, bp2_co
1354 if (percent1
>= -0.02 and percent1
<= 1.02):
1355 _temp_co
, percent2
= intersect_point_line(
1356 intersec_coords
[1], bp3_co
, bp4_co
1358 if (percent2
>= -0.02 and percent2
<= 1.02):
1359 # Format: spline index, first point index from
1360 # corresponding segment, percentage from first point of
1361 # actual segment, coords of intersection point
1362 all_intersections
.append(
1364 ob_splines
.matrix_world
@ intersec_coords
[0])
1366 all_intersections
.append(
1368 ob_splines
.matrix_world
@ intersec_coords
[1])
1371 checked_splines
.append(i
)
1372 # Sort list by spline, then by corresponding first point index of segment,
1373 # and then by percentage from first point of segment: elements 0 and 1 respectively
1374 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1376 self
.crosshatch_strokes_coords
= {}
1377 for i
in range(len(all_intersections
)):
1378 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1379 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1381 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1382 all_intersections
[i
][3]
1383 ) # Save intersection coords
1385 self
.is_crosshatch
= False
1387 # Delete all duplicates
1388 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1390 # If the main object has modifiers, turn their "viewport view status" to
1391 # what it was before the forced deactivation above
1392 if len(self
.main_object
.modifiers
) > 0:
1393 for m_idx
in range(len(self
.main_object
.modifiers
)):
1394 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1400 # Part of the Crosshatch process that is repeated when the operator is tweaked
1401 def crosshatch_surface_execute(self
, context
):
1402 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1403 # (without this the surface verts merging with the main object doesn't work well)
1404 self
.modifiers_prev_viewport_state
= []
1405 if len(self
.main_object
.modifiers
) > 0:
1406 for m_idx
in range(len(self
.main_object
.modifiers
)):
1407 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1409 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1411 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1413 me_name
= "SURFSKIO_STK_TMP"
1414 me
= bpy
.data
.meshes
.new(me_name
)
1416 all_verts_coords
= []
1418 for st_idx
in self
.crosshatch_strokes_coords
:
1419 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1420 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1422 all_verts_coords
.append(coords
)
1425 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1427 me
.from_pydata(all_verts_coords
, all_edges
, [])
1428 ob
= object_utils
.object_data_add(context
, me
)
1429 ob
.location
= (0.0, 0.0, 0.0)
1430 ob
.rotation_euler
= (0.0, 0.0, 0.0)
1431 ob
.scale
= (1.0, 1.0, 1.0)
1433 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1435 bpy
.context
.view_layer
.objects
.active
= ob
1437 # Get together each vert and its nearest, to the middle position
1438 verts
= ob
.data
.vertices
1440 for i
in range(len(verts
)):
1441 shortest_dist
= None
1443 if i
not in checked_verts
:
1444 for t
in range(len(verts
)):
1445 if i
!= t
and t
not in checked_verts
:
1446 dist
= (verts
[i
].co
- verts
[t
].co
).length
1448 if shortest_dist
is not None:
1449 if dist
< shortest_dist
:
1450 shortest_dist
= dist
1453 shortest_dist
= dist
1456 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1458 verts
[i
].co
= middle_location
1459 verts
[nearest_vert
].co
= middle_location
1461 checked_verts
.append(i
)
1462 checked_verts
.append(nearest_vert
)
1464 # Calculate average length between all the generated edges
1465 ob
= bpy
.context
.object
1467 for ed
in ob
.data
.edges
:
1468 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1469 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1471 lengths_sum
+= (v1
.co
- v2
.co
).length
1473 edges_count
= len(ob
.data
.edges
)
1474 # possible division by zero here
1475 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1477 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1478 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1479 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1480 threshold
=average_edge_length
/ 15.0)
1481 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1483 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1485 # Make a dictionary with the verts related to each vert
1486 related_key_verts
= {}
1487 for ed
in final_points_ob
.data
.edges
:
1488 if not ed
.vertices
[0] in related_key_verts
:
1489 related_key_verts
[ed
.vertices
[0]] = []
1491 if not ed
.vertices
[1] in related_key_verts
:
1492 related_key_verts
[ed
.vertices
[1]] = []
1494 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1495 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1497 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1498 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1500 # Get groups of verts forming each face
1501 faces_verts_idx
= []
1502 for v1
in related_key_verts
: # verts-1 ....
1503 for v2
in related_key_verts
: # verts-2
1505 related_verts_in_common
= []
1506 v2_in_rel_v1
= False
1507 v1_in_rel_v2
= False
1508 for rel_v1
in related_key_verts
[v1
]:
1509 # Check if related verts of verts-1 are related verts of verts-2
1510 if rel_v1
in related_key_verts
[v2
]:
1511 related_verts_in_common
.append(rel_v1
)
1513 if v2
in related_key_verts
[v1
]:
1516 if v1
in related_key_verts
[v2
]:
1519 repeated_face
= False
1520 # If two verts have two related verts in common, they form a quad
1521 if len(related_verts_in_common
) == 2:
1522 # Check if the face is already saved
1523 for f_verts
in faces_verts_idx
:
1526 if len(f_verts
) == 4:
1531 if related_verts_in_common
[0] in f_verts
:
1533 if related_verts_in_common
[1] in f_verts
:
1536 if repeated_verts
== len(f_verts
):
1537 repeated_face
= True
1540 if not repeated_face
:
1541 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1542 v2
, related_verts_in_common
[1]])
1544 # If Two verts have one related vert in common and they are
1545 # related to each other, they form a triangle
1546 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1547 # Check if the face is already saved.
1548 for f_verts
in faces_verts_idx
:
1551 if len(f_verts
) == 3:
1556 if related_verts_in_common
[0] in f_verts
:
1559 if repeated_verts
== len(f_verts
):
1560 repeated_face
= True
1563 if not repeated_face
:
1564 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1566 # Keep only the faces that don't overlap by ignoring
1567 # quads that overlap with two adjacent triangles
1568 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1569 for i
in range(len(faces_verts_idx
)):
1570 for t
in range(len(faces_verts_idx
)):
1574 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1575 for v_idx
in faces_verts_idx
[t
]:
1576 if v_idx
in faces_verts_idx
[i
]:
1577 verts_in_common
+= 1
1578 # If it doesn't have all it's vertices repeated in the other face
1579 if verts_in_common
== 3:
1580 if i
not in faces_to_not_include_idx
:
1581 faces_to_not_include_idx
.append(i
)
1584 all_surface_verts_co
= []
1585 for i
in range(len(final_points_ob
.data
.vertices
)):
1586 coords
= final_points_ob
.data
.vertices
[i
].co
1587 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1589 # Verts of each face.
1590 all_surface_faces
= []
1591 for i
in range(len(faces_verts_idx
)):
1592 if i
not in faces_to_not_include_idx
:
1594 for v_idx
in faces_verts_idx
[i
]:
1597 all_surface_faces
.append(face
)
1600 surf_me_name
= "SURFSKIO_surface"
1601 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1602 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1603 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1604 ob_surface
.location
= (0.0, 0.0, 0.0)
1605 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
1606 ob_surface
.scale
= (1.0, 1.0, 1.0)
1608 # Delete final points temporal object
1609 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1611 # Delete isolated verts if there are any
1612 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1613 ob_surface
.select_set(True)
1614 bpy
.context
.view_layer
.objects
.active
= ob_surface
1616 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1617 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1618 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1619 bpy
.ops
.mesh
.delete()
1620 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1622 # Join crosshatch results with original mesh
1624 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1625 edges_length_sum
= 0
1626 for ed
in ob_surface
.data
.edges
:
1627 edges_length_sum
+= (
1628 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1629 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1632 # Make dictionary with all the verts connected to each vert, on the new surface object.
1633 surface_connected_verts
= {}
1634 for ed
in ob_surface
.data
.edges
:
1635 if not ed
.vertices
[0] in surface_connected_verts
:
1636 surface_connected_verts
[ed
.vertices
[0]] = []
1638 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1640 if ed
.vertices
[1] not in surface_connected_verts
:
1641 surface_connected_verts
[ed
.vertices
[1]] = []
1643 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1645 # Duplicate the new surface object, and use shrinkwrap to
1646 # calculate later the nearest verts to the main object
1647 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1648 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1649 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1651 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1653 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1655 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1656 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1657 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1658 shrinkwrap_modifier
.target
= self
.main_object
1660 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap_modifier
.name
)
1662 # Make list with verts of original mesh as index and coords as value
1663 main_object_verts_coords
= []
1664 for v
in self
.main_object
.data
.vertices
:
1665 coords
= self
.main_object
.matrix_world
@ v
.co
1667 # To avoid problems when taking "-0.00" as a different value as "0.00"
1668 for c
in range(len(coords
)):
1669 if "%.3f" % coords
[c
] == "-0.00":
1672 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1674 tuple(main_object_verts_coords
)
1676 # Determine which verts will be merged, snap them to the nearest verts
1677 # on the original verts, and get them selected
1678 crosshatch_verts_to_merge
= []
1679 if self
.automatic_join
:
1680 for i
in range(len(ob_surface
.data
.vertices
)-1):
1681 # Calculate the distance from each of the connected verts to the actual vert,
1682 # and compare it with the distance they would have if joined.
1683 # If they don't change much, that vert can be joined
1684 merge_actual_vert
= True
1686 if len(surface_connected_verts
[i
]) < 4:
1687 for c_v_idx
in surface_connected_verts
[i
]:
1688 points_original
= []
1689 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1690 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1693 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1694 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1696 vec_A
= points_original
[0] - points_original
[1]
1697 vec_B
= points_target
[0] - points_target
[1]
1699 dist_A
= (points_original
[0] - points_original
[1]).length
1700 dist_B
= (points_target
[0] - points_target
[1]).length
1703 points_original
[0] == points_original
[1] or
1704 points_target
[0] == points_target
[1]
1705 ): # If any vector's length is zero
1707 angle
= vec_A
.angle(vec_B
) / pi
1711 # Set a range of acceptable variation in the connected edges
1712 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1713 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1714 angle
>= 0.15 * self
.join_stretch_factor
:
1716 merge_actual_vert
= False
1719 merge_actual_vert
= False
1721 self
.report({'WARNING'},
1722 "Crosshatch set incorrectly")
1724 if merge_actual_vert
:
1725 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1726 # To avoid problems when taking "-0.000" as a different value as "0.00"
1727 for c
in range(len(coords
)):
1728 if "%.3f" % coords
[c
] == "-0.00":
1731 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1733 if comparison_coords
in main_object_verts_coords
:
1734 # Get the index of the vert with those coords in the main object
1735 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1737 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1738 self
.main_object_selected_verts_count
== 0:
1740 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1741 ob_surface
.data
.vertices
[i
].select
= True
1742 crosshatch_verts_to_merge
.append(i
)
1744 # Make sure the vert in the main object is selected,
1745 # in case it wasn't selected and the "join crosshatch" option is active
1746 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1748 # Delete duplicated object
1749 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1751 # Join crosshatched surface and main object
1752 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1753 ob_surface
.select_set(True)
1754 self
.main_object
.select_set(True)
1755 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1757 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1759 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1760 # Perform Remove doubles to merge verts
1761 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1762 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1764 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1766 # If the main object has modifiers, turn their "viewport view status"
1767 # to what it was before the forced deactivation above
1768 if len(self
.main_object
.modifiers
) > 0:
1769 for m_idx
in range(len(self
.main_object
.modifiers
)):
1770 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1776 def rectangular_surface(self
, context
):
1778 all_selected_edges_idx
= []
1779 all_selected_verts
= []
1781 for ed
in self
.main_object
.data
.edges
:
1783 all_selected_edges_idx
.append(ed
.index
)
1786 if not ed
.vertices
[0] in all_selected_verts
:
1787 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1788 if not ed
.vertices
[1] in all_selected_verts
:
1789 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1791 # All verts (both from each edge) to determine later
1792 # which are at the tips (those not repeated twice)
1793 all_verts_idx
.append(ed
.vertices
[0])
1794 all_verts_idx
.append(ed
.vertices
[1])
1796 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1797 all_chains_tips_idx
= []
1798 for v_idx
in all_verts_idx
:
1799 if all_verts_idx
.count(v_idx
) < 2:
1800 all_chains_tips_idx
.append(v_idx
)
1802 edges_connected_to_tips
= []
1803 for ed
in self
.main_object
.data
.edges
:
1804 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1805 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1807 edges_connected_to_tips
.append(ed
)
1809 # Check closed selections
1810 # List with groups of three verts, where the first element of the pair is
1811 # the unselected vert of a closed selection and the other two elements are the
1812 # selected neighbor verts (it will be useful to determine which selection chain
1813 # the unselected vert belongs to, and determine the "middle-vertex")
1814 single_unselected_verts_and_neighbors
= []
1816 # To identify a "closed" selection (a selection that is a closed chain except
1817 # for one vertex) find the vertex in common that have the edges connected to tips.
1818 # If there is a vertex in common, that one is the unselected vert that closes
1819 # the selection or is a "middle-vertex"
1820 single_unselected_verts
= []
1821 for ed
in edges_connected_to_tips
:
1822 for ed_b
in edges_connected_to_tips
:
1824 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1825 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1826 ed
.vertices
[0] not in single_unselected_verts
:
1828 # The second element is one of the tips of the selected
1829 # vertices of the closed selection
1830 single_unselected_verts_and_neighbors
.append(
1831 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1833 single_unselected_verts
.append(ed
.vertices
[0])
1835 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1836 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1837 ed
.vertices
[0] not in single_unselected_verts
:
1839 single_unselected_verts_and_neighbors
.append(
1840 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1842 single_unselected_verts
.append(ed
.vertices
[0])
1844 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1845 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1846 ed
.vertices
[1] not in single_unselected_verts
:
1848 single_unselected_verts_and_neighbors
.append(
1849 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1851 single_unselected_verts
.append(ed
.vertices
[1])
1853 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1854 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1855 ed
.vertices
[1] not in single_unselected_verts
:
1857 single_unselected_verts_and_neighbors
.append(
1858 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1860 single_unselected_verts
.append(ed
.vertices
[1])
1863 middle_vertex_idx
= None
1864 tips_to_discard_idx
= []
1866 # Check if there is a "middle-vertex", and get its index
1867 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1868 actual_chain_verts
= self
.get_ordered_verts(
1869 self
.main_object
, all_selected_edges_idx
,
1870 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1874 if single_unselected_verts_and_neighbors
[i
][2] != \
1875 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1877 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1878 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1879 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1881 # List with pairs of verts that belong to the tips of each selection chain (row)
1882 verts_tips_same_chain_idx
= []
1883 if len(all_chains_tips_idx
) >= 2:
1885 for i
in range(0, len(all_chains_tips_idx
)):
1886 if all_chains_tips_idx
[i
] not in checked_v
:
1887 v_chain
= self
.get_ordered_verts(
1888 self
.main_object
, all_selected_edges_idx
,
1889 all_verts_idx
, all_chains_tips_idx
[i
],
1890 middle_vertex_idx
, None
1893 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1895 checked_v
.append(v_chain
[0].index
)
1896 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1898 # Selection tips (vertices).
1899 verts_tips_parsed_idx
= []
1900 if len(all_chains_tips_idx
) >= 2:
1901 for spec_v_idx
in all_chains_tips_idx
:
1902 if (spec_v_idx
not in tips_to_discard_idx
):
1903 verts_tips_parsed_idx
.append(spec_v_idx
)
1905 # Identify the type of selection made by the user
1906 if middle_vertex_idx
is not None:
1907 # If there are 4 tips (two selection chains), and
1908 # there is only one single unselected vert (the middle vert)
1909 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1910 selection_type
= "TWO_CONNECTED"
1912 # The type of the selection was not identified, the script stops.
1913 self
.report({'WARNING'}, "The selection isn't valid.")
1915 self
.stopping_errors
= True
1919 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1920 selection_type
= "SINGLE"
1921 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1922 selection_type
= "TWO_NOT_CONNECTED"
1923 elif len(all_chains_tips_idx
) == 0:
1924 if len(self
.main_splines
.data
.splines
) > 1:
1925 selection_type
= "NO_SELECTION"
1927 # If the selection was not identified and there is only one stroke,
1928 # there's no possibility to build a surface, so the script is interrupted
1929 self
.report({'WARNING'}, "The selection isn't valid.")
1931 self
.stopping_errors
= True
1935 # The type of the selection was not identified, the script stops
1936 self
.report({'WARNING'}, "The selection isn't valid.")
1938 self
.stopping_errors
= True
1942 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1943 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1944 self
.report({'WARNING'},
1945 "At least two strokes are needed when there are two not connected selections")
1947 self
.stopping_errors
= True
1951 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1953 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1954 self
.main_splines
.select_set(True)
1955 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1957 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1958 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1959 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1960 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1961 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1962 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1963 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1964 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1965 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1967 self
.selection_U_exists
= False
1968 self
.selection_U2_exists
= False
1969 self
.selection_V_exists
= False
1970 self
.selection_V2_exists
= False
1972 self
.selection_U_is_closed
= False
1973 self
.selection_U2_is_closed
= False
1974 self
.selection_V_is_closed
= False
1975 self
.selection_V2_is_closed
= False
1977 # Define what vertices are at the tips of each selection and are not the middle-vertex
1978 if selection_type
== "TWO_CONNECTED":
1979 self
.selection_U_exists
= True
1980 self
.selection_V_exists
= True
1982 closing_vert_U_idx
= None
1983 closing_vert_V_idx
= None
1984 closing_vert_U2_idx
= None
1985 closing_vert_V2_idx
= None
1987 # Determine which selection is Selection-U and which is Selection-V
1990 points_first_stroke_tips
= []
1993 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
1996 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
1999 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2002 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2004 points_first_stroke_tips
.append(
2005 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2007 points_first_stroke_tips
.append(
2008 self
.main_splines
.data
.splines
[0].bezier_points
[
2009 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2013 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2014 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2016 if angle_A
< angle_B
:
2017 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2018 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2020 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2021 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2023 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2024 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2025 last_sketched_point_first_stroke_co
= \
2026 self
.main_splines
.data
.splines
[0].bezier_points
[
2027 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2029 first_sketched_point_last_stroke_co
= \
2030 self
.main_splines
.data
.splines
[
2031 len(self
.main_splines
.data
.splines
) - 1
2032 ].bezier_points
[0].co
2033 if len(self
.main_splines
.data
.splines
) > 1:
2034 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2035 last_sketched_point_second_stroke_co
= \
2036 self
.main_splines
.data
.splines
[1].bezier_points
[
2037 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2040 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2041 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2042 single_unselected_neighbors
.append(verts_neig_idx
[1])
2043 single_unselected_neighbors
.append(verts_neig_idx
[2])
2045 all_chains_tips_and_middle_vert
= []
2046 for v_idx
in all_chains_tips_idx
:
2047 if v_idx
not in single_unselected_neighbors
:
2048 all_chains_tips_and_middle_vert
.append(v_idx
)
2050 all_chains_tips_and_middle_vert
+= single_unselected_verts
2052 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2054 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2055 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2056 self
.shortest_distance(
2058 first_sketched_point_first_stroke_co
,
2059 all_chains_tips_and_middle_vert
2061 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2062 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2063 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2065 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2067 nearest_tip_to_first_st_first_pt_idx
,
2068 verts_tips_same_chain_idx
2070 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2071 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2072 self
.shortest_distance(
2074 last_sketched_point_first_stroke_co
,
2075 all_chains_tips_and_middle_vert
2077 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2078 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2079 self
.shortest_distance(
2081 first_sketched_point_last_stroke_co
,
2082 all_chains_tips_and_middle_vert
2084 if len(self
.main_splines
.data
.splines
) > 1:
2085 # The selected vertex nearest to the first point of the second sketched stroke
2086 # (This will be useful to determine the direction of the closed
2087 # selection V when extruding along strokes)
2088 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2089 self
.shortest_distance(
2091 first_sketched_point_second_stroke_co
,
2094 # The selected vertex nearest to the first point of the second sketched stroke
2095 # (This will be useful to determine the direction of the closed
2096 # selection V2 when extruding along strokes)
2097 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2098 self
.shortest_distance(
2100 last_sketched_point_second_stroke_co
,
2103 # Determine if the single selection will be treated as U or as V
2105 for i
in all_selected_edges_idx
:
2107 (self
.main_object
.matrix_world
@
2108 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2109 (self
.main_object
.matrix_world
@
2110 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2113 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2115 # Get shortest distance from the first point of the last stroke to any participating vertex
2116 _temp_idx
, shortest_distance_to_last_stroke
= \
2117 self
.shortest_distance(
2119 first_sketched_point_last_stroke_co
,
2120 all_participating_verts
2122 # If the beginning of the first stroke is near enough, and its orientation
2123 # difference with the first edge of the nearest selection chain is not too high,
2124 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2125 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2126 shortest_distance_to_last_stroke
< average_edge_length
and \
2127 len(self
.main_splines
.data
.splines
) > 1:
2129 self
.selection_U_exists
= False
2130 self
.selection_V_exists
= True
2131 # If the first selection is not closed
2132 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2133 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2134 self
.selection_V_is_closed
= False
2135 closing_vert_U_idx
= None
2136 closing_vert_U2_idx
= None
2137 closing_vert_V_idx
= None
2138 closing_vert_V2_idx
= None
2140 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2142 if selection_type
== "TWO_NOT_CONNECTED":
2143 self
.selection_V2_exists
= True
2145 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2147 self
.selection_V_is_closed
= True
2148 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2150 # Get the neighbors of the first (unselected) vert of the closed selection U.
2152 for verts
in single_unselected_verts_and_neighbors
:
2153 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2154 vert_neighbors
.append(verts
[1])
2155 vert_neighbors
.append(verts
[2])
2158 verts_V
= self
.get_ordered_verts(
2159 self
.main_object
, all_selected_edges_idx
,
2160 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2163 for i
in range(0, len(verts_V
)):
2164 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2165 # If the vertex nearest to the first point of the second stroke
2166 # is in the first half of the selected verts
2167 if i
>= len(verts_V
) / 2:
2168 first_vert_V_idx
= vert_neighbors
[1]
2171 first_vert_V_idx
= vert_neighbors
[0]
2174 if selection_type
== "TWO_NOT_CONNECTED":
2175 self
.selection_V2_exists
= True
2176 # If the second selection is not closed
2177 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2178 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2180 self
.selection_V2_is_closed
= False
2181 closing_vert_V2_idx
= None
2182 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2185 self
.selection_V2_is_closed
= True
2186 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2188 # Get the neighbors of the first (unselected) vert of the closed selection U
2190 for verts
in single_unselected_verts_and_neighbors
:
2191 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2192 vert_neighbors
.append(verts
[1])
2193 vert_neighbors
.append(verts
[2])
2196 verts_V2
= self
.get_ordered_verts(
2197 self
.main_object
, all_selected_edges_idx
,
2198 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2201 for i
in range(0, len(verts_V2
)):
2202 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2203 # If the vertex nearest to the first point of the second stroke
2204 # is in the first half of the selected verts
2205 if i
>= len(verts_V2
) / 2:
2206 first_vert_V2_idx
= vert_neighbors
[1]
2209 first_vert_V2_idx
= vert_neighbors
[0]
2212 self
.selection_V2_exists
= False
2215 self
.selection_U_exists
= True
2216 self
.selection_V_exists
= False
2217 # If the first selection is not closed
2218 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2219 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2220 self
.selection_U_is_closed
= False
2221 closing_vert_U_idx
= None
2225 self
.main_object
.matrix_world
@
2226 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2229 self
.main_object
.matrix_world
@
2230 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2232 points_first_stroke_tips
= []
2233 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2234 points_first_stroke_tips
.append(
2235 self
.main_splines
.data
.splines
[0].bezier_points
[
2236 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2239 vec_A
= points_tips
[0] - points_tips
[1]
2240 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2242 # Compare the direction of the selection and the first
2243 # grease pencil stroke to determine which is the "first" vertex of the selection
2244 if vec_A
.dot(vec_B
) < 0:
2245 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2247 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2250 self
.selection_U_is_closed
= True
2251 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2253 # Get the neighbors of the first (unselected) vert of the closed selection U
2255 for verts
in single_unselected_verts_and_neighbors
:
2256 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2257 vert_neighbors
.append(verts
[1])
2258 vert_neighbors
.append(verts
[2])
2261 points_first_and_neighbor
= []
2262 points_first_and_neighbor
.append(
2263 self
.main_object
.matrix_world
@
2264 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2266 points_first_and_neighbor
.append(
2267 self
.main_object
.matrix_world
@
2268 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2270 points_first_stroke_tips
= []
2271 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2272 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2274 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2275 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2277 # Compare the direction of the selection and the first grease pencil stroke to
2278 # determine which is the vertex neighbor to the first vertex (unselected) of
2279 # the closed selection. This will determine the direction of the closed selection
2280 if vec_A
.dot(vec_B
) < 0:
2281 first_vert_U_idx
= vert_neighbors
[1]
2283 first_vert_U_idx
= vert_neighbors
[0]
2285 if selection_type
== "TWO_NOT_CONNECTED":
2286 self
.selection_U2_exists
= True
2287 # If the second selection is not closed
2288 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2289 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2291 self
.selection_U2_is_closed
= False
2292 closing_vert_U2_idx
= None
2293 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2295 self
.selection_U2_is_closed
= True
2296 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2298 # Get the neighbors of the first (unselected) vert of the closed selection U
2300 for verts
in single_unselected_verts_and_neighbors
:
2301 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2302 vert_neighbors
.append(verts
[1])
2303 vert_neighbors
.append(verts
[2])
2306 points_first_and_neighbor
= []
2307 points_first_and_neighbor
.append(
2308 self
.main_object
.matrix_world
@
2309 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2311 points_first_and_neighbor
.append(
2312 self
.main_object
.matrix_world
@
2313 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2315 points_last_stroke_tips
= []
2316 points_last_stroke_tips
.append(
2317 self
.main_splines
.data
.splines
[
2318 len(self
.main_splines
.data
.splines
) - 1
2319 ].bezier_points
[0].co
2321 points_last_stroke_tips
.append(
2322 self
.main_splines
.data
.splines
[
2323 len(self
.main_splines
.data
.splines
) - 1
2324 ].bezier_points
[1].co
2326 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2327 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2329 # Compare the direction of the selection and the last grease pencil stroke to
2330 # determine which is the vertex neighbor to the first vertex (unselected) of
2331 # the closed selection. This will determine the direction of the closed selection
2332 if vec_A
.dot(vec_B
) < 0:
2333 first_vert_U2_idx
= vert_neighbors
[1]
2335 first_vert_U2_idx
= vert_neighbors
[0]
2337 self
.selection_U2_exists
= False
2339 elif selection_type
== "NO_SELECTION":
2340 self
.selection_U_exists
= False
2341 self
.selection_V_exists
= False
2343 # Get an ordered list of the vertices of Selection-U
2344 verts_ordered_U
= []
2345 if self
.selection_U_exists
:
2346 verts_ordered_U
= self
.get_ordered_verts(
2347 self
.main_object
, all_selected_edges_idx
,
2348 all_verts_idx
, first_vert_U_idx
,
2349 middle_vertex_idx
, closing_vert_U_idx
2352 # Get an ordered list of the vertices of Selection-U2
2353 verts_ordered_U2
= []
2354 if self
.selection_U2_exists
:
2355 verts_ordered_U2
= self
.get_ordered_verts(
2356 self
.main_object
, all_selected_edges_idx
,
2357 all_verts_idx
, first_vert_U2_idx
,
2358 middle_vertex_idx
, closing_vert_U2_idx
2361 # Get an ordered list of the vertices of Selection-V
2362 verts_ordered_V
= []
2363 if self
.selection_V_exists
:
2364 verts_ordered_V
= self
.get_ordered_verts(
2365 self
.main_object
, all_selected_edges_idx
,
2366 all_verts_idx
, first_vert_V_idx
,
2367 middle_vertex_idx
, closing_vert_V_idx
2369 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2371 # Get an ordered list of the vertices of Selection-V2
2372 verts_ordered_V2
= []
2373 if self
.selection_V2_exists
:
2374 verts_ordered_V2
= self
.get_ordered_verts(
2375 self
.main_object
, all_selected_edges_idx
,
2376 all_verts_idx
, first_vert_V2_idx
,
2377 middle_vertex_idx
, closing_vert_V2_idx
2380 # Check if when there are two-not-connected selections both have the same
2381 # number of verts. If not terminate the script
2382 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2383 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2385 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2387 self
.stopping_errors
= True
2391 # Calculate edges U proportions
2392 # Sum selected edges U lengths
2393 edges_lengths_U
= []
2394 edges_lengths_sum_U
= 0
2396 if self
.selection_U_exists
:
2397 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2401 if self
.selection_U2_exists
:
2402 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2406 # Sum selected edges V lengths
2407 edges_lengths_V
= []
2408 edges_lengths_sum_V
= 0
2410 if self
.selection_V_exists
:
2411 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2415 if self
.selection_V2_exists
:
2416 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2421 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2422 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2423 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2424 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2427 edges_proportions_U
= []
2428 edges_proportions_U
= self
.get_edges_proportions(
2429 edges_lengths_U
, edges_lengths_sum_U
,
2430 self
.selection_U_exists
, self
.edges_U
2432 verts_count_U
= len(edges_proportions_U
) + 1
2434 if self
.selection_U2_exists
:
2435 edges_proportions_U2
= []
2436 edges_proportions_U2
= self
.get_edges_proportions(
2437 edges_lengths_U2
, edges_lengths_sum_U2
,
2438 self
.selection_U2_exists
, self
.edges_V
2442 edges_proportions_V
= []
2443 edges_proportions_V
= self
.get_edges_proportions(
2444 edges_lengths_V
, edges_lengths_sum_V
,
2445 self
.selection_V_exists
, self
.edges_V
2448 if self
.selection_V2_exists
:
2449 edges_proportions_V2
= []
2450 edges_proportions_V2
= self
.get_edges_proportions(
2451 edges_lengths_V2
, edges_lengths_sum_V2
,
2452 self
.selection_V2_exists
, self
.edges_V
2455 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2456 # the actual sketched curves with a "closing segment"
2457 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2458 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2459 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2461 simplified_spline_coords
= []
2462 simplified_curve
= []
2463 ob_simplified_curve
= []
2464 splines_first_v_co
= []
2465 for i
in range(len(self
.main_splines
.data
.splines
)):
2466 # Create a curve object for the actual spline "cyclic extension"
2467 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2468 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2469 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2471 simplified_curve
[i
].dimensions
= "3D"
2474 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2475 spline_coords
.append(bp
.co
)
2478 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2480 # Get the coordinates of the first vert of the actual spline
2481 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2483 # Generate the spline
2484 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2485 # less one because one point is added when the spline is created
2486 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2487 for p
in range(0, len(simplified_spline_coords
[i
])):
2488 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2490 spline
.use_cyclic_u
= True
2492 spline_bp_count
= len(spline
.bezier_points
)
2494 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2495 ob_simplified_curve
[i
].select_set(True)
2496 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2498 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2499 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2500 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2501 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2502 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2504 # Select the "closing segment", and subdivide it
2505 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2506 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2507 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2509 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2510 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2511 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2513 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2515 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2516 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2517 self
.average_gp_segment_length
2520 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=int(segments
))
2522 # Delete the other vertices and make it non-cyclic to
2523 # keep only the needed verts of the "closing segment"
2524 bpy
.ops
.curve
.select_all(action
='INVERT')
2525 bpy
.ops
.curve
.delete(type='VERT')
2526 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2527 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2529 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2530 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2531 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2532 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2534 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2535 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2536 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2538 # Delete the temporal curve
2539 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2541 # Get the coords of the points distributed along the sketched strokes,
2542 # with proportions-U of the first selection
2543 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2544 self
.main_splines
.data
.splines
,
2547 sketched_splines_parsed
= []
2549 if self
.selection_U2_exists
:
2550 # Initialize the multidimensional list with the proportions of all the segments
2551 proportions_loops_crossing_strokes
= []
2552 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2553 proportions_loops_crossing_strokes
.append([])
2555 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2556 proportions_loops_crossing_strokes
[i
].append(None)
2558 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2559 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2560 loop_segments_lengths
= []
2562 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2563 # When on the first stroke, add the segment from the selection to the dirst stroke
2565 loop_segments_lengths
.append(
2566 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2567 pts_on_strokes_with_proportions_U
[0][lp
]).length
2569 # For all strokes except for the last, calculate the distance
2570 # from the actual stroke to the next
2571 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2572 loop_segments_lengths
.append(
2573 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2574 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2576 # When on the last stroke, add the segments
2577 # from the last stroke to the second selection
2578 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2579 loop_segments_lengths
.append(
2580 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2581 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2583 # Calculate full loop length
2584 loop_seg_lengths_sum
= 0
2585 for i
in range(len(loop_segments_lengths
)):
2586 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2588 # Fill the multidimensional list with the proportions of all the segments
2589 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2590 proportions_loops_crossing_strokes
[st
][lp
] = \
2591 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2593 # Calculate proportions for each stroke
2594 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2595 actual_stroke_spline
= []
2596 # Needs to be a list for the "distribute_pts" method
2597 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2599 # Calculate the proportions for the actual stroke.
2600 actual_edges_proportions_U
= []
2601 for i
in range(len(edges_proportions_U
)):
2604 # Sum the proportions of this loop up to the actual.
2605 for t
in range(0, st
+ 1):
2606 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2607 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2608 # and the proportions refer to edges, so we start at the element 1
2609 # of proportions_loops_crossing_strokes instead of element 0
2610 actual_edges_proportions_U
.append(
2611 edges_proportions_U
[i
] -
2612 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2614 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2615 sketched_splines_parsed
.append(points_actual_spline
[0])
2617 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2619 # If the selection type is "TWO_NOT_CONNECTED" replace the
2620 # points of the last spline with the points in the "target" selection
2621 if selection_type
== "TWO_NOT_CONNECTED":
2622 if self
.selection_U2_exists
:
2623 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2624 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2625 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2627 # Create temporary curves along the "control-points" found
2628 # on the sketched curves and the mesh selection
2629 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2630 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2631 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2632 ob_ctrl_pts
.data
= me
2633 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2640 for i
in range(0, verts_count_U
):
2641 vert_num_in_spline
= 1
2643 if self
.selection_U_exists
:
2644 ob_ctrl_pts
.data
.vertices
.add(1)
2645 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2646 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2648 vert_num_in_spline
+= 1
2650 for t
in range(0, len(sketched_splines_parsed
)):
2651 ob_ctrl_pts
.data
.vertices
.add(1)
2652 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2653 v
.co
= sketched_splines_parsed
[t
][i
]
2655 if vert_num_in_spline
> 1:
2656 ob_ctrl_pts
.data
.edges
.add(1)
2657 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2658 len(ob_ctrl_pts
.data
.vertices
) - 2
2659 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2660 len(ob_ctrl_pts
.data
.vertices
) - 1
2663 first_verts
.append(v
.index
)
2666 second_verts
.append(v
.index
)
2668 if t
== len(sketched_splines_parsed
) - 1:
2669 last_verts
.append(v
.index
)
2672 vert_num_in_spline
+= 1
2674 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2675 ob_ctrl_pts
.select_set(True)
2676 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2678 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2679 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2680 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2682 # Determine which loops-U will be "Cyclic"
2683 for i
in range(0, len(first_verts
)):
2684 # When there is Cyclic Cross there is no need of
2685 # Automatic Join, (and there are at least three strokes)
2686 if self
.automatic_join
and not self
.cyclic_cross
and \
2687 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2689 v
= ob_ctrl_pts
.data
.vertices
2690 first_point_co
= v
[first_verts
[i
]].co
2691 second_point_co
= v
[second_verts
[i
]].co
2692 last_point_co
= v
[last_verts
[i
]].co
2694 # Coordinates of the point in the center of both the first and last verts.
2696 (first_point_co
[0] + last_point_co
[0]) / 2,
2697 (first_point_co
[1] + last_point_co
[1]) / 2,
2698 (first_point_co
[2] + last_point_co
[2]) / 2
2700 vec_A
= second_point_co
- first_point_co
2701 vec_B
= second_point_co
- Vector(verts_center_co
)
2703 # Calculate the length of the first segment of the loop,
2704 # and the length it would have after moving the first vert
2705 # to the middle position between first and last
2706 length_original
= (second_point_co
- first_point_co
).length
2707 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2709 angle
= vec_A
.angle(vec_B
) / pi
2711 # If the target length doesn't stretch too much, and the
2712 # its angle doesn't change to much either
2713 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2714 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2716 cyclic_loops_U
.append(True)
2717 # Move the first vert to the center coordinates
2718 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2719 # Select the last verts from Cyclic loops, for later deletion all at once
2720 v
[last_verts
[i
]].select
= True
2722 cyclic_loops_U
.append(False)
2724 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2725 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2726 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2727 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2729 cyclic_loops_U
.append(True)
2731 cyclic_loops_U
.append(False)
2733 # The cyclic_loops_U list needs to be reversed.
2734 cyclic_loops_U
.reverse()
2736 # Delete the previously selected (last_)verts.
2737 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2738 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2739 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2741 # Create curves from control points.
2742 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2743 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2744 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2745 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2746 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2748 # Make Cyclic the splines designated as Cyclic.
2749 for i
in range(0, len(cyclic_loops_U
)):
2750 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2752 # Get the coords of all points on first loop-U, for later comparison with its
2753 # subdivided version, to know which points of the loops-U are crossed by the
2754 # original strokes. The indices will be the same for the other loops-U
2755 if self
.loops_on_strokes
:
2756 coords_loops_U_control_points
= []
2757 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2758 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2760 tuple(coords_loops_U_control_points
)
2762 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2763 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2764 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2766 edges_V_count
= len(edges_proportions_V
)
2768 # The Follow precision will vary depending on the number of Follow face-loops
2769 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2770 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2772 # Subdivide the curves
2773 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2775 # The verts position shifting that happens with splines subdivision.
2776 # For later reorder splines points
2777 verts_position_shift
= curve_cuts
+ 1
2778 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2780 # Reorder coordinates of the points of each spline to put the first point of
2781 # the spline starting at the position it was the first point before sudividing
2782 # the curve. And make a new curve object per spline (to handle memory better later)
2783 splines_U_objects
= []
2784 for i
in range(len(ob_curves_surf
.data
.splines
)):
2785 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2786 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2787 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2789 spline_U_curve
.dimensions
= "3D"
2791 # Add points to the spline in the new curve object
2792 ob_spline_U
.data
.splines
.new('BEZIER')
2793 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2794 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2795 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2796 point_index
= t
+ verts_position_shift
2798 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2801 # to avoid adding the first point since it's added when the spline is created
2803 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2804 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2805 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2807 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2808 # Add a last point at the same location as the first one
2809 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2810 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2811 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2813 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2815 splines_U_objects
.append(ob_spline_U
)
2816 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2817 ob_spline_U
.select_set(True)
2818 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2820 # When option "Loops on strokes" is active each "Cross" loop will have
2821 # its own proportions according to where the original strokes "touch" them
2822 if self
.loops_on_strokes
:
2823 # Get the indices of points where the original strokes "touch" loops-U
2824 points_U_crossed_by_strokes
= []
2825 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2826 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2827 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2828 points_U_crossed_by_strokes
.append(i
)
2830 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2831 edge_order_number_for_splines
= {}
2832 if self
.selection_V_exists
:
2833 # For two-connected selections add a first hypothetic stroke at the beginning.
2834 if selection_type
== "TWO_CONNECTED":
2835 edge_order_number_for_splines
[0] = 0
2837 for i
in range(len(self
.main_splines
.data
.splines
)):
2838 sp
= self
.main_splines
.data
.splines
[i
]
2839 v_idx
, _dist_temp
= self
.shortest_distance(
2841 sp
.bezier_points
[0].co
,
2842 verts_ordered_V_indices
2844 # Get the position (edges count) of the vert v_idx in the selected chain V
2845 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2847 # For two-connected selections the strokes go after the
2848 # hypothetic stroke added before, so the index adds one per spline
2849 if selection_type
== "TWO_CONNECTED":
2850 spline_number
= i
+ 1
2854 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2856 # Get the first and last verts indices for later comparison
2859 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2862 if self
.selection_V_is_closed
:
2863 # If there is no last stroke on the last vertex (same as first vertex),
2864 # add a hypothetic spline at last vert order
2865 if first_v_idx
!= last_v_idx
:
2866 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2867 len(verts_ordered_V_indices
) - 1
2869 if self
.cyclic_cross
:
2870 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2871 len(verts_ordered_V_indices
) - 2
2872 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2873 len(verts_ordered_V_indices
) - 1
2875 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2876 len(verts_ordered_V_indices
) - 1
2878 # Get the coords of the points distributed along the
2879 # "crossing curves", with appropriate proportions-V
2880 surface_splines_parsed
= []
2881 for i
in range(len(splines_U_objects
)):
2882 sp_ob
= splines_U_objects
[i
]
2883 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2884 if self
.loops_on_strokes
:
2885 # Segments distances from stroke to stroke
2888 segments_distances
= []
2889 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2890 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2896 dist
+= (last_p
- actual_p
).length
2898 if t
in points_U_crossed_by_strokes
:
2899 segments_distances
.append(dist
)
2906 # Calculate Proportions.
2907 used_edges_proportions_V
= []
2908 for t
in range(len(segments_distances
)):
2909 if self
.selection_V_exists
:
2911 order_number_last_stroke
= 0
2913 segment_edges_length_V
= 0
2914 segment_edges_length_V2
= 0
2915 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2916 segment_edges_length_V
+= edges_lengths_V
[order
]
2917 if self
.selection_V2_exists
:
2918 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2920 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2921 # Calculate each "sub-segment" (the ones between each stroke) length
2922 if self
.selection_V2_exists
:
2923 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2924 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2925 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2926 (segment_edges_length_V2
- segment_edges_length_V
) /
2927 len(splines_U_objects
) * i
)
2929 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2931 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2932 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2934 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2936 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2939 for _c
in range(self
.edges_V
):
2940 # Calculate each "sub-segment" (the ones between each stroke) length
2941 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2942 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2944 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2945 surface_splines_parsed
.append(actual_spline
[0])
2948 if self
.selection_V2_exists
:
2949 used_edges_proportions_V
= []
2950 for p
in range(len(edges_proportions_V
)):
2951 used_edges_proportions_V
.append(
2952 edges_proportions_V2
[p
] -
2953 ((edges_proportions_V2
[p
] -
2954 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2957 used_edges_proportions_V
= edges_proportions_V
2959 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2960 surface_splines_parsed
.append(actual_spline
[0])
2962 # Set the verts of the first and last splines to the locations
2963 # of the respective verts in the selections
2964 if self
.selection_V_exists
:
2965 for i
in range(0, len(surface_splines_parsed
[0])):
2966 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2967 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2969 if selection_type
== "TWO_NOT_CONNECTED":
2970 if self
.selection_V2_exists
:
2971 for i
in range(0, len(surface_splines_parsed
[0])):
2972 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2974 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2975 # merge the verts of the tips of the loops when they are "near enough"
2976 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2977 # Join the tips of "Follow" loops that are near enough and must be "closed"
2978 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2979 for i
in range(len(surface_splines_parsed
[0])):
2980 sp
= surface_splines_parsed
2981 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2983 verts_middle_position_co
= [
2984 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2985 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2986 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2988 points_original
= []
2989 points_original
.append(sp
[1][i
])
2990 points_original
.append(sp
[0][i
])
2993 points_target
.append(sp
[1][i
])
2994 points_target
.append(Vector(verts_middle_position_co
))
2996 vec_A
= points_original
[0] - points_original
[1]
2997 vec_B
= points_target
[0] - points_target
[1]
2998 # check for zero angles, not sure if it is a great fix
2999 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3000 angle
= vec_A
.angle(vec_B
) / pi
3001 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3006 # If after moving the verts to the middle point, the segment doesn't stretch too much
3007 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3008 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3010 # Avoid joining when the actual loop must be merged with the original mesh
3011 if not (self
.selection_U_exists
and i
== 0) and \
3012 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3014 # Change the coords of both verts to the middle position
3015 surface_splines_parsed
[0][i
] = verts_middle_position_co
3016 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3018 # Delete object with control points and object from grease pencil conversion
3019 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3021 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3025 # Get all verts coords
3026 all_surface_verts_co
= []
3027 for i
in range(0, len(surface_splines_parsed
)):
3028 # Get coords of all verts and make a list with them
3029 for pt_co
in surface_splines_parsed
[i
]:
3030 all_surface_verts_co
.append(pt_co
)
3032 # Define verts for each face
3033 all_surface_faces
= []
3034 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3035 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3036 all_surface_faces
.append(
3037 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3038 i
+ len(surface_splines_parsed
[0]) + 1]
3041 surf_me_name
= "SURFSKIO_surface"
3042 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3043 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3044 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3045 ob_surface
.location
= (0.0, 0.0, 0.0)
3046 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3047 ob_surface
.scale
= (1.0, 1.0, 1.0)
3049 # Select all the "unselected but participating" verts, from closed selection
3050 # or double selections with middle-vertex, for later join with remove doubles
3051 for v_idx
in single_unselected_verts
:
3052 self
.main_object
.data
.vertices
[v_idx
].select
= True
3054 # Join the new mesh to the main object
3055 ob_surface
.select_set(True)
3056 self
.main_object
.select_set(True)
3057 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3059 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3061 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3063 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3064 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3065 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3073 global global_shade_smooth
3074 if global_shade_smooth
:
3075 bpy
.ops
.object.shade_smooth()
3077 bpy
.ops
.object.shade_flat()
3078 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3084 def execute(self
, context
):
3086 if bpy
.ops
.object.mode_set
.poll():
3087 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3090 global global_mesh_object
3091 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3092 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3093 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3094 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3095 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3097 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3099 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3103 if not self
.is_fill_faces
:
3104 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3105 value
='True, False, False')
3107 # Build splines from the "last saved splines".
3108 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3109 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3110 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3112 last_saved_curve
.dimensions
= "3D"
3114 for sp
in self
.last_strokes_splines_coords
:
3115 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3116 # less one because one point is added when the spline is created
3117 spline
.bezier_points
.add(len(sp
) - 1)
3118 for p
in range(0, len(sp
)):
3119 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3121 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3123 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3124 self
.main_splines
.select_set(True)
3125 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3127 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3129 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3130 # Important to make it vector first and then automatic, otherwise the
3131 # tips handles get too big and distort the shrinkwrap results later
3132 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3133 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3134 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3135 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3137 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3139 if self
.is_crosshatch
:
3140 strokes_for_crosshatch
= True
3141 strokes_for_rectangular_surface
= False
3143 strokes_for_rectangular_surface
= True
3144 strokes_for_crosshatch
= False
3146 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3148 if strokes_for_rectangular_surface
:
3149 self
.rectangular_surface(context
)
3150 elif strokes_for_crosshatch
:
3151 self
.crosshatch_surface_execute(context
)
3153 #Set Shade smooth to new polygons
3154 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3155 global global_shade_smooth
3156 if global_shade_smooth
:
3157 bpy
.ops
.object.shade_smooth()
3159 bpy
.ops
.object.shade_flat()
3161 # Delete main splines
3162 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3163 if self
.keep_strokes
:
3164 self
.main_splines
.name
= "keep_strokes"
3165 self
.main_splines
.data
.bevel_depth
= 0.001
3166 if "keep_strokes_material" in bpy
.data
.materials
:
3167 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3169 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3170 mat
.diffuse_color
= (1, 0, 0, 0)
3171 mat
.specular_color
= (1, 0, 0)
3172 mat
.specular_intensity
= 0.0
3174 self
.main_splines
.data
.materials
.append(mat
)
3176 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3178 # Delete grease pencil strokes
3179 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3181 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3185 # Delete annotations
3186 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3188 bpy
.context
.annotation_data
.layers
.active
.clear()
3192 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3193 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3194 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3195 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3196 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3197 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3198 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3199 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3201 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3202 self
.main_object
.select_set(True)
3203 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3205 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3211 def invoke(self
, context
, event
):
3213 if bpy
.ops
.object.mode_set
.poll():
3214 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3216 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3217 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3218 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3219 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3220 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3221 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3224 global global_mesh_object
3225 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3226 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3227 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3228 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3230 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3235 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3237 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3238 value
='True, False, False')
3240 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3241 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3243 self
.is_fill_faces
= False
3244 self
.stopping_errors
= False
3245 self
.last_strokes_splines_coords
= []
3247 # Determine the type of the strokes
3248 self
.strokes_type
= get_strokes_type(context
)
3250 # Check if it will be used grease pencil strokes or curves
3251 # If there are strokes to be used
3252 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3253 if self
.strokes_type
== "GP_STROKES":
3254 # Convert grease pencil strokes to curve
3255 global global_gpencil_object
3256 gp
= bpy
.data
.objects
[global_gpencil_object
]
3257 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3258 self
.using_external_curves
= False
3260 elif self
.strokes_type
== "GP_ANNOTATION":
3261 # Convert grease pencil strokes to curve
3262 gp
= bpy
.context
.annotation_data
3263 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3264 self
.using_external_curves
= False
3266 elif self
.strokes_type
== "EXTERNAL_CURVE":
3267 global global_curve_object
3268 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3269 self
.using_external_curves
= True
3271 # Make sure there are no objects left from erroneous
3272 # executions of this operator, with the reserved names used here
3273 for o
in bpy
.data
.objects
:
3274 if o
.name
.find("SURFSKIO_") != -1:
3275 bpy
.ops
.object.delete({"selected_objects": [o
]})
3277 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3279 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3281 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3283 # Deselect all points of the curve
3284 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3285 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3286 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3288 # Delete splines with only a single isolated point
3289 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3290 sp
= self
.temporary_curve
.data
.splines
[i
]
3292 if len(sp
.bezier_points
) == 1:
3293 sp
.bezier_points
[0].select_control_point
= True
3295 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3296 bpy
.ops
.curve
.delete(type='VERT')
3297 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3299 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3300 self
.temporary_curve
.select_set(True)
3301 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3303 # Set a minimum number of points for crosshatch
3304 minimum_points_num
= 15
3306 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3307 # Check if the number of points of each curve has at least the number of points
3308 # of minimum_points_num, which is a bit more than the face-loops limit.
3309 # If not, subdivide to reach at least that number of points
3310 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3311 sp
= self
.temporary_curve
.data
.splines
[i
]
3313 if len(sp
.bezier_points
) < minimum_points_num
:
3314 for bp
in sp
.bezier_points
:
3315 bp
.select_control_point
= True
3317 if (len(sp
.bezier_points
) - 1) != 0:
3318 # Formula to get the number of cuts that will make a curve
3319 # of N number of points have near to "minimum_points_num"
3320 # points, when subdividing with this number of cuts
3321 subdivide_cuts
= int(
3322 (minimum_points_num
- len(sp
.bezier_points
)) /
3323 (len(sp
.bezier_points
) - 1)
3328 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3329 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3331 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3333 # Detect if the strokes are a crosshatch and do it if it is
3334 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3336 if not self
.is_crosshatch
:
3337 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3338 self
.temporary_curve
.select_set(True)
3339 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3341 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3343 # Set a minimum number of points for rectangular surfaces
3344 minimum_points_num
= 60
3346 # Check if the number of points of each curve has at least the number of points
3347 # of minimum_points_num, which is a bit more than the face-loops limit.
3348 # If not, subdivide to reach at least that number of points
3349 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3350 sp
= self
.temporary_curve
.data
.splines
[i
]
3352 if len(sp
.bezier_points
) < minimum_points_num
:
3353 for bp
in sp
.bezier_points
:
3354 bp
.select_control_point
= True
3356 if (len(sp
.bezier_points
) - 1) != 0:
3357 # Formula to get the number of cuts that will make a curve of
3358 # N number of points have near to "minimum_points_num" points,
3359 # when subdividing with this number of cuts
3360 subdivide_cuts
= int(
3361 (minimum_points_num
- len(sp
.bezier_points
)) /
3362 (len(sp
.bezier_points
) - 1)
3367 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3368 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3370 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3372 # Save coordinates of the actual strokes (as the "last saved splines")
3373 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3374 self
.last_strokes_splines_coords
.append([])
3375 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3376 coords
= self
.temporary_curve
.matrix_world
@ \
3377 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3378 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3380 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3381 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3382 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3383 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3384 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3385 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3388 (first_p_co
[0] + last_p_co
[0]) / 2,
3389 (first_p_co
[1] + last_p_co
[1]) / 2,
3390 (first_p_co
[2] + last_p_co
[2]) / 2
3393 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3394 self
.last_strokes_splines_coords
[sp_idx
][
3395 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3397 tuple(self
.last_strokes_splines_coords
)
3399 # Estimation of the average length of the segments between
3400 # each point of the grease pencil strokes.
3401 # Will be useful to determine whether a curve should be made "Cyclic"
3402 segments_lengths_sum
= 0
3404 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3405 for i
in range(0, len(random_spline
)):
3406 if i
!= 0 and len(random_spline
) - 1 >= i
:
3407 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3410 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3412 # Delete temporary strokes curve object
3413 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3415 # Set again since "execute()" will turn it again to its initial value
3416 self
.execute(context
)
3418 if not self
.stopping_errors
:
3419 # Delete grease pencil strokes
3420 if self
.strokes_type
== "GP_STROKES":
3422 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3426 # Delete annotation strokes
3427 elif self
.strokes_type
== "GP_ANNOTATION":
3429 bpy
.context
.annotation_data
.layers
.active
.clear()
3433 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3434 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3435 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3441 elif self
.strokes_type
== "SELECTION_ALONE":
3442 self
.is_fill_faces
= True
3443 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3445 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3447 if created_faces_count
== 0:
3448 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3449 return {"CANCELLED"}
3453 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3454 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3457 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3458 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3461 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3462 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3464 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3467 elif self
.strokes_type
== "NO_STROKES":
3468 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3471 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3472 self
.report({'WARNING'}, "All splines must be Bezier.")
3478 # ----------------------------
3480 class MESH_OT_SURFSK_init(Operator
):
3481 bl_idname
= "mesh.surfsk_init"
3482 bl_label
= "Bsurfaces initialize"
3483 bl_description
= "Add an empty mesh object with useful settings"
3484 bl_options
= {'REGISTER', 'UNDO'}
3486 def execute(self
, context
):
3488 bs
= bpy
.context
.scene
.bsurfaces
3490 if bpy
.ops
.object.mode_set
.poll():
3491 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3493 global global_shade_smooth
3494 global global_mesh_object
3495 global global_gpencil_object
3497 if bs
.SURFSK_mesh
== None:
3498 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3499 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3500 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3501 mesh_object
.select_set(True)
3502 bpy
.context
.view_layer
.objects
.active
= mesh_object
3504 mesh_object
.show_all_edges
= True
3505 mesh_object
.display_type
= 'SOLID'
3506 mesh_object
.show_wire
= True
3508 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3509 if global_shade_smooth
:
3510 bpy
.ops
.object.shade_smooth()
3512 bpy
.ops
.object.shade_flat()
3514 color_red
= [1.0, 0.0, 0.0, 0.3]
3515 material
= makeMaterial("BSurfaceMesh", color_red
)
3516 mesh_object
.data
.materials
.append(material
)
3517 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3518 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3519 if self
.active_object
is not None:
3520 modifier
.target
= self
.active_object
3521 modifier
.wrap_method
= 'TARGET_PROJECT'
3522 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3523 modifier
.show_on_cage
= True
3525 global_mesh_object
= mesh_object
.name
3526 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3528 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3529 bpy
.context
.scene
.tool_settings
.use_snap
= True
3530 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3531 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3532 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3533 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3534 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3536 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3537 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3539 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3540 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3541 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')
3542 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3543 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3544 gpencil_object
.select_set(True)
3545 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3546 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3547 global_gpencil_object
= gpencil_object
.name
3548 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3549 gpencil_object
.data
.stroke_depth_order
= '3D'
3550 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3551 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3553 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3554 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3555 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3557 def invoke(self
, context
, event
):
3558 if bpy
.context
.active_object
:
3559 self
.active_object
= bpy
.context
.active_object
3561 self
.active_object
= None
3563 self
.execute(context
)
3567 # ----------------------------
3568 # Add modifiers operator
3569 class MESH_OT_SURFSK_add_modifiers(Operator
):
3570 bl_idname
= "mesh.surfsk_add_modifiers"
3571 bl_label
= "Add Mirror and others modifiers"
3572 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3573 bl_options
= {'REGISTER', 'UNDO'}
3575 def execute(self
, context
):
3577 bs
= bpy
.context
.scene
.bsurfaces
3579 if bpy
.ops
.object.mode_set
.poll():
3580 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3582 if bs
.SURFSK_mesh
== None:
3583 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3585 mesh_object
= bs
.SURFSK_mesh
3588 mesh_object
.select_set(True)
3590 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3593 bpy
.context
.view_layer
.objects
.active
= mesh_object
3596 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3597 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3598 shrinkwrap
.target
= self
.active_object
3599 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3600 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3601 shrinkwrap
.show_on_cage
= True
3602 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3604 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3605 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3606 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3607 shrinkwrap
.target
= self
.active_object
3608 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3609 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3610 shrinkwrap
.show_on_cage
= True
3611 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3614 mirror
= mesh_object
.modifiers
["Mirror"]
3615 mirror
.use_clip
= True
3617 bpy
.ops
.object.modifier_add(type='MIRROR')
3618 mirror
= mesh_object
.modifiers
["Mirror"]
3619 mirror
.use_clip
= True
3622 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3624 bpy
.ops
.object.modifier_add(type='SUBSURF')
3625 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3628 solidify
= mesh_object
.modifiers
["Solidify"]
3629 solidify
.thickness
= 0.01
3631 bpy
.ops
.object.modifier_add(type='SOLIDIFY')
3632 solidify
= mesh_object
.modifiers
["Solidify"]
3633 solidify
.thickness
= 0.01
3637 def invoke(self
, context
, event
):
3638 if bpy
.context
.active_object
:
3639 self
.active_object
= bpy
.context
.active_object
3641 self
.active_object
= None
3643 self
.execute(context
)
3647 # ----------------------------
3648 # Edit surface operator
3649 class MESH_OT_SURFSK_edit_surface(Operator
):
3650 bl_idname
= "mesh.surfsk_edit_surface"
3651 bl_label
= "Bsurfaces edit surface"
3652 bl_description
= "Edit surface mesh"
3653 bl_options
= {'REGISTER', 'UNDO'}
3655 def execute(self
, context
):
3656 if bpy
.ops
.object.mode_set
.poll():
3657 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3658 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3659 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3660 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3661 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3662 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3664 def invoke(self
, context
, event
):
3666 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3667 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3668 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3669 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3671 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3674 self
.execute(context
)
3678 # ----------------------------
3679 # Add strokes operator
3680 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3681 bl_idname
= "gpencil.surfsk_add_strokes"
3682 bl_label
= "Bsurfaces add strokes"
3683 bl_description
= "Add the grease pencil strokes"
3684 bl_options
= {'REGISTER', 'UNDO'}
3686 def execute(self
, context
):
3687 if bpy
.ops
.object.mode_set
.poll():
3688 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3689 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3691 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3692 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3693 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3694 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3698 def invoke(self
, context
, event
):
3700 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3702 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3705 self
.execute(context
)
3709 # ----------------------------
3710 # Edit strokes operator
3711 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3712 bl_idname
= "gpencil.surfsk_edit_strokes"
3713 bl_label
= "Bsurfaces edit strokes"
3714 bl_description
= "Edit the grease pencil strokes"
3715 bl_options
= {'REGISTER', 'UNDO'}
3717 def execute(self
, context
):
3718 if bpy
.ops
.object.mode_set
.poll():
3719 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3720 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3722 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3724 gpencil_object
.select_set(True)
3725 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3727 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3729 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3733 def invoke(self
, context
, event
):
3735 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3737 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3740 self
.execute(context
)
3744 # ----------------------------
3745 # Convert annotation to curves operator
3746 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3747 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3748 bl_label
= "Convert annotation to curves"
3749 bl_description
= "Convert annotation to curves for editing"
3750 bl_options
= {'REGISTER', 'UNDO'}
3752 def execute(self
, context
):
3754 if bpy
.ops
.object.mode_set
.poll():
3755 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3757 # Convert annotation to curve
3758 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3761 # Delete annotation strokes
3763 bpy
.context
.annotation_data
.layers
.active
.clear()
3768 curve
.select_set(True)
3769 bpy
.context
.view_layer
.objects
.active
= curve
3771 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3775 def invoke(self
, context
, event
):
3777 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3779 _strokes_num
= len(strokes
)
3781 self
.report({'WARNING'}, "Not active annotation")
3784 self
.execute(context
)
3788 # ----------------------------
3789 # Convert strokes to curves operator
3790 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3791 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3792 bl_label
= "Convert strokes to curves"
3793 bl_description
= "Convert grease pencil strokes to curves for editing"
3794 bl_options
= {'REGISTER', 'UNDO'}
3796 def execute(self
, context
):
3798 if bpy
.ops
.object.mode_set
.poll():
3799 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3801 # Convert grease pencil strokes to curve
3802 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3803 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3806 # Delete grease pencil strokes
3808 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3814 curve
.select_set(True)
3815 bpy
.context
.view_layer
.objects
.active
= curve
3817 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3821 def invoke(self
, context
, event
):
3823 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3825 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3828 self
.execute(context
)
3832 # ----------------------------
3834 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3835 bl_idname
= "gpencil.surfsk_add_annotation"
3836 bl_label
= "Bsurfaces add annotation"
3837 bl_description
= "Add annotation"
3838 bl_options
= {'REGISTER', 'UNDO'}
3840 def execute(self
, context
):
3841 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3842 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3846 def invoke(self
, context
, event
):
3848 self
.execute(context
)
3853 # ----------------------------
3854 # Edit curve operator
3855 class CURVE_OT_SURFSK_edit_curve(Operator
):
3856 bl_idname
= "curve.surfsk_edit_curve"
3857 bl_label
= "Bsurfaces edit curve"
3858 bl_description
= "Edit curve"
3859 bl_options
= {'REGISTER', 'UNDO'}
3861 def execute(self
, context
):
3862 if bpy
.ops
.object.mode_set
.poll():
3863 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3864 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3865 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3866 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3867 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3869 def invoke(self
, context
, event
):
3871 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3873 self
.report({'WARNING'}, "Specify the name of the object with curve")
3876 self
.execute(context
)
3880 # ----------------------------
3882 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3883 bl_idname
= "curve.surfsk_reorder_splines"
3884 bl_label
= "Bsurfaces reorder splines"
3885 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3886 bl_options
= {'REGISTER', 'UNDO'}
3888 def execute(self
, context
):
3889 objects_to_delete
= []
3890 # Convert grease pencil strokes to curve.
3891 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3892 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3893 for ob
in bpy
.context
.selected_objects
:
3894 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3895 GP_strokes_curve
= ob
3897 # GP_strokes_curve = bpy.context.object
3898 objects_to_delete
.append(GP_strokes_curve
)
3900 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3901 GP_strokes_curve
.select_set(True)
3902 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3904 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3905 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3906 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3907 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3909 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3910 GP_strokes_mesh
= bpy
.context
.object
3911 objects_to_delete
.append(GP_strokes_mesh
)
3913 GP_strokes_mesh
.data
.resolution_u
= 1
3914 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3916 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3917 self
.main_curve
.select_set(True)
3918 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3920 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3921 curves_duplicate_1
= bpy
.context
.object
3922 objects_to_delete
.append(curves_duplicate_1
)
3924 minimum_points_num
= 500
3926 # Some iterations since the subdivision operator
3927 # has a limit of 100 subdivisions per iteration
3928 for x
in range(round(minimum_points_num
/ 100)):
3929 # Check if the number of points of each curve has at least the number of points
3930 # of minimum_points_num. If not, subdivide to reach at least that number of points
3931 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3932 sp
= curves_duplicate_1
.data
.splines
[i
]
3934 if len(sp
.bezier_points
) < minimum_points_num
:
3935 for bp
in sp
.bezier_points
:
3936 bp
.select_control_point
= True
3938 if (len(sp
.bezier_points
) - 1) != 0:
3939 # Formula to get the number of cuts that will make a curve of N
3940 # number of points have near to "minimum_points_num" points,
3941 # when subdividing with this number of cuts
3942 subdivide_cuts
= int(
3943 (minimum_points_num
- len(sp
.bezier_points
)) /
3944 (len(sp
.bezier_points
) - 1)
3949 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3950 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3951 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3952 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3954 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3955 curves_duplicate_2
= bpy
.context
.object
3956 objects_to_delete
.append(curves_duplicate_2
)
3958 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3959 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3960 curves_duplicate_2
.select_set(True)
3961 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3963 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3964 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
3965 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
3966 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
='Shrinkwrap')
3968 # Get the distance of each vert from its original position to its position with Shrinkwrap
3969 nearest_points_coords
= {}
3970 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3971 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3972 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3973 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3975 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3976 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3979 shortest_dist
= (bp_1_co
- bp_2_co
).length
3980 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3981 "%.4f" % bp_2_co
[1],
3982 "%.4f" % bp_2_co
[2])
3984 dist
= (bp_1_co
- bp_2_co
).length
3986 if dist
< shortest_dist
:
3987 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3988 "%.4f" % bp_2_co
[1],
3989 "%.4f" % bp_2_co
[2])
3990 shortest_dist
= dist
3992 # Get all coords of GP strokes points, for comparison
3993 GP_strokes_coords
= []
3994 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
3995 GP_strokes_coords
.append(
3996 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
3997 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
3998 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
3999 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4002 # Check the point of the GP strokes with the same coords as
4003 # the nearest points of the curves (with shrinkwrap)
4005 # Dictionary with GP stroke index as index, and a list as value.
4006 # The list has as index the point index of the GP stroke
4007 # nearest to the spline, and as value the spline index
4008 GP_connection_points
= {}
4009 for gp_st_idx
in range(len(GP_strokes_coords
)):
4010 GPvert_spline_relationship
= {}
4012 for splines_st_idx
in range(len(nearest_points_coords
)):
4013 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4014 GPvert_spline_relationship
[
4015 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4018 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4020 # Get the splines new order
4021 splines_new_order
= []
4022 for i
in GP_connection_points
:
4023 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4026 splines_new_order
.append(GP_connection_points
[i
][k
])
4029 curve_original_name
= self
.main_curve
.name
4031 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4032 self
.main_curve
.select_set(True)
4033 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4035 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4037 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4038 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4039 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4041 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4042 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4044 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4045 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4046 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4048 # Get the names of the separated splines objects in the original order
4049 splines_unordered
= {}
4050 for o
in bpy
.data
.objects
:
4051 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4052 spline_order_string
= o
.name
.partition(".")[2]
4054 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4055 spline_order_index
= int(spline_order_string
) - 1
4056 splines_unordered
[spline_order_index
] = o
.name
4058 # Join all splines objects in final order
4059 for order_idx
in splines_new_order
:
4060 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4061 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4062 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4063 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4065 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4067 # Go back to the original name of the curves object.
4068 bpy
.context
.object.name
= curve_original_name
4070 # Delete all unused objects
4071 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
4073 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4074 bpy
.data
.objects
[curve_original_name
].select_set(True)
4075 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4077 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4078 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4081 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4088 def invoke(self
, context
, event
):
4089 self
.main_curve
= bpy
.context
.object
4090 there_are_GP_strokes
= False
4093 # Get the active grease pencil layer
4094 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4097 there_are_GP_strokes
= True
4101 if there_are_GP_strokes
:
4102 self
.execute(context
)
4103 self
.report({'INFO'}, "Splines have been reordered")
4105 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4109 # ----------------------------
4110 # Set first points operator
4111 class CURVE_OT_SURFSK_first_points(Operator
):
4112 bl_idname
= "curve.surfsk_first_points"
4113 bl_label
= "Bsurfaces set first points"
4114 bl_description
= "Set the selected points as the first point of each spline"
4115 bl_options
= {'REGISTER', 'UNDO'}
4117 def execute(self
, context
):
4118 splines_to_invert
= []
4120 # Check non-cyclic splines to invert
4121 for i
in range(len(self
.main_curve
.data
.splines
)):
4122 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4124 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4125 if b_points
[len(b_points
) - 1].select_control_point
:
4126 splines_to_invert
.append(i
)
4128 # Reorder points of cyclic splines, and set all handles to "Automatic"
4130 # Check first selected point
4131 cyclic_splines_new_first_pt
= {}
4132 for i
in self
.cyclic_splines
:
4133 sp
= self
.main_curve
.data
.splines
[i
]
4135 for t
in range(len(sp
.bezier_points
)):
4136 bp
= sp
.bezier_points
[t
]
4137 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4138 cyclic_splines_new_first_pt
[i
] = t
4139 break # To take only one if there are more
4142 for spline_idx
in cyclic_splines_new_first_pt
:
4143 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4145 spline_old_coords
= []
4146 for bp_old
in sp
.bezier_points
:
4147 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4149 left_handle_type
= str(bp_old
.handle_left_type
)
4150 left_handle_length
= float(bp_old
.handle_left
.length
)
4152 float(bp_old
.handle_left
.x
),
4153 float(bp_old
.handle_left
.y
),
4154 float(bp_old
.handle_left
.z
)
4156 right_handle_type
= str(bp_old
.handle_right_type
)
4157 right_handle_length
= float(bp_old
.handle_right
.length
)
4158 right_handle_xyz
= (
4159 float(bp_old
.handle_right
.x
),
4160 float(bp_old
.handle_right
.y
),
4161 float(bp_old
.handle_right
.z
)
4163 spline_old_coords
.append(
4164 [coords
, left_handle_type
,
4165 right_handle_type
, left_handle_length
,
4166 right_handle_length
, left_handle_xyz
,
4170 for t
in range(len(sp
.bezier_points
)):
4171 bp
= sp
.bezier_points
4173 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4174 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4176 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4178 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4180 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4181 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4183 bp
[t
].handle_left_type
= "FREE"
4184 bp
[t
].handle_right_type
= "FREE"
4186 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4187 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4188 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4190 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4191 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4192 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4194 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4195 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4197 # Invert the non-cyclic splines designated above
4198 for i
in range(len(splines_to_invert
)):
4199 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4201 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4202 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4203 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4205 bpy
.ops
.curve
.switch_direction()
4207 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4209 # Keep selected the first vert of each spline
4210 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4211 for i
in range(len(self
.main_curve
.data
.splines
)):
4212 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4213 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4215 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4216 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4219 bp
.select_control_point
= True
4220 bp
.select_right_handle
= True
4221 bp
.select_left_handle
= True
4223 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4227 def invoke(self
, context
, event
):
4228 self
.main_curve
= bpy
.context
.object
4230 # Check if all curves are Bezier, and detect which ones are cyclic
4231 self
.cyclic_splines
= []
4232 for i
in range(len(self
.main_curve
.data
.splines
)):
4233 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4234 self
.report({'WARNING'}, "All splines must be Bezier type")
4236 return {'CANCELLED'}
4238 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4239 self
.cyclic_splines
.append(i
)
4241 self
.execute(context
)
4242 self
.report({'INFO'}, "First points have been set")
4247 # Add-ons Preferences Update Panel
4249 # Define Panel classes for updating
4251 VIEW3D_PT_tools_SURFSK_mesh
,
4252 VIEW3D_PT_tools_SURFSK_curve
4256 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4257 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4258 newCurve
.dimensions
= '3D'
4259 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4262 if type == 'GPensil':
4264 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4267 CurveObject
.location
= pencil
.location
4268 CurveObject
.rotation_euler
= pencil
.rotation_euler
4269 CurveObject
.scale
= pencil
.scale
4270 elif type == 'Annotation':
4272 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4275 CurveObject
.location
= (0.0, 0.0, 0.0)
4276 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4277 CurveObject
.scale
= (1.0, 1.0, 1.0)
4280 for i
, _stroke
in enumerate(strokes
):
4281 stroke_points
= strokes
[i
].points
4282 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4283 for point
in stroke_points
]
4284 points_to_add
= len(data_list
)-1
4287 for point
in data_list
:
4288 flat_list
.extend(point
)
4290 spline
= newCurve
.splines
.new(type='BEZIER')
4291 spline
.bezier_points
.add(points_to_add
)
4292 spline
.bezier_points
.foreach_set("co", flat_list
)
4294 for point
in spline
.bezier_points
:
4295 point
.handle_left_type
="AUTO"
4296 point
.handle_right_type
="AUTO"
4303 def update_panel(self
, context
):
4304 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4306 for panel
in panels
:
4307 if "bl_rna" in panel
.__dict
__:
4308 bpy
.utils
.unregister_class(panel
)
4310 for panel
in panels
:
4311 category
= context
.preferences
.addons
[__name__
].preferences
.category
4312 if category
!= 'Tool':
4313 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4315 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4316 panel
.bl_category
= 'Edit'
4317 raise ValueError("You can not install add-ons in the Tool panel")
4318 bpy
.utils
.register_class(panel
)
4320 except Exception as e
:
4321 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4324 def makeMaterial(name
, diffuse
):
4326 if name
in bpy
.data
.materials
:
4327 material
= bpy
.data
.materials
[name
]
4328 material
.diffuse_color
= diffuse
4330 material
= bpy
.data
.materials
.new(name
)
4331 material
.diffuse_color
= diffuse
4335 def update_mesh(self
, context
):
4337 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4338 bpy
.ops
.object.select_all(action
='DESELECT')
4339 bpy
.context
.view_layer
.update()
4340 global global_mesh_object
4341 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4342 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4343 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4345 print("Select mesh object")
4347 def update_gpencil(self
, context
):
4349 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4350 bpy
.ops
.object.select_all(action
='DESELECT')
4351 bpy
.context
.view_layer
.update()
4352 global global_gpencil_object
4353 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4354 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4355 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4357 print("Select gpencil object")
4359 def update_curve(self
, context
):
4361 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4362 bpy
.ops
.object.select_all(action
='DESELECT')
4363 bpy
.context
.view_layer
.update()
4364 global global_curve_object
4365 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4366 bpy
.data
.objects
[global_curve_object
].select_set(True)
4367 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4369 print("Select curve object")
4371 def update_shade_smooth(self
, context
):
4373 global global_shade_smooth
4374 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4376 contex_mode
= bpy
.context
.mode
4378 if bpy
.ops
.object.mode_set
.poll():
4379 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4381 bpy
.ops
.object.select_all(action
='DESELECT')
4382 global global_mesh_object
4383 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4384 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4386 if global_shade_smooth
:
4387 bpy
.ops
.object.shade_smooth()
4389 bpy
.ops
.object.shade_flat()
4391 if contex_mode
== "EDIT_MESH":
4392 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4395 print("Select mesh object")
4398 class BsurfPreferences(AddonPreferences
):
4399 # this must match the addon name, use '__package__'
4400 # when defining this in a submodule of a python package.
4401 bl_idname
= __name__
4403 category
: StringProperty(
4404 name
="Tab Category",
4405 description
="Choose a name for the category of the panel",
4410 def draw(self
, context
):
4411 layout
= self
.layout
4415 col
.label(text
="Tab Category:")
4416 col
.prop(self
, "category", text
="")
4419 class BsurfacesProps(PropertyGroup
):
4420 SURFSK_guide
: EnumProperty(
4423 ('Annotation', 'Annotation', 'Annotation'),
4424 ('GPencil', 'GPencil', 'GPencil'),
4425 ('Curve', 'Curve', 'Curve')
4427 default
="Annotation"
4429 SURFSK_edges_U
: IntProperty(
4431 description
="Number of face-loops crossing the strokes",
4436 SURFSK_edges_V
: IntProperty(
4438 description
="Number of face-loops following the strokes",
4443 SURFSK_cyclic_cross
: BoolProperty(
4444 name
="Cyclic Cross",
4445 description
="Make cyclic the face-loops crossing the strokes",
4448 SURFSK_cyclic_follow
: BoolProperty(
4449 name
="Cyclic Follow",
4450 description
="Make cyclic the face-loops following the strokes",
4453 SURFSK_keep_strokes
: BoolProperty(
4454 name
="Keep strokes",
4455 description
="Keeps the sketched strokes or curves after adding the surface",
4458 SURFSK_automatic_join
: BoolProperty(
4459 name
="Automatic join",
4460 description
="Join automatically vertices of either surfaces "
4461 "generated by crosshatching, or from the borders of closed shapes",
4464 SURFSK_loops_on_strokes
: BoolProperty(
4465 name
="Loops on strokes",
4466 description
="Make the loops match the paths of the strokes",
4469 SURFSK_precision
: IntProperty(
4471 description
="Precision level of the surface calculation",
4476 SURFSK_mesh
: PointerProperty(
4477 name
="Mesh of BSurface",
4478 type=bpy
.types
.Object
,
4479 description
="Mesh of BSurface",
4482 SURFSK_gpencil
: PointerProperty(
4483 name
="GreasePencil object",
4484 type=bpy
.types
.Object
,
4485 description
="GreasePencil object",
4486 update
=update_gpencil
,
4488 SURFSK_curve
: PointerProperty(
4489 name
="Curve object",
4490 type=bpy
.types
.Object
,
4491 description
="Curve object",
4492 update
=update_curve
,
4494 SURFSK_shade_smooth
: BoolProperty(
4495 name
="Shade smooth",
4496 description
="Render and display faces smooth, using interpolated Vertex Normals",
4498 update
=update_shade_smooth
,
4502 MESH_OT_SURFSK_init
,
4503 MESH_OT_SURFSK_add_modifiers
,
4504 MESH_OT_SURFSK_add_surface
,
4505 MESH_OT_SURFSK_edit_surface
,
4506 GPENCIL_OT_SURFSK_add_strokes
,
4507 GPENCIL_OT_SURFSK_edit_strokes
,
4508 GPENCIL_OT_SURFSK_strokes_to_curves
,
4509 GPENCIL_OT_SURFSK_annotation_to_curves
,
4510 GPENCIL_OT_SURFSK_add_annotation
,
4511 CURVE_OT_SURFSK_edit_curve
,
4512 CURVE_OT_SURFSK_reorder_splines
,
4513 CURVE_OT_SURFSK_first_points
,
4520 bpy
.utils
.register_class(cls
)
4522 for panel
in panels
:
4523 bpy
.utils
.register_class(panel
)
4525 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4526 update_panel(None, bpy
.context
)
4529 for panel
in panels
:
4530 bpy
.utils
.unregister_class(panel
)
4533 bpy
.utils
.unregister_class(cls
)
4535 del bpy
.types
.Scene
.bsurfaces
4537 if __name__
== "__main__":