1 # SPDX-FileCopyrightText: 2010-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "name": "Bsurfaces GPL Edition",
8 "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
10 "blender": (2, 80, 0),
11 "location": "View3D EditMode > Sidebar > Edit Tab",
12 "description": "Modeling and retopology tool",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
20 from bpy_extras
import object_utils
23 from mathutils
import Matrix
, Vector
24 from mathutils
.geometry
import (
33 from bpy
.props
import (
42 from bpy
.types
import (
49 # ----------------------------
51 global_shade_smooth
= False
52 global_mesh_object
= ""
53 global_gpencil_object
= ""
54 global_curve_object
= ""
56 # ----------------------------
58 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
59 bl_space_type
= 'VIEW_3D'
62 bl_label
= "Bsurfaces"
64 def draw(self
, context
):
66 bs
= context
.scene
.bsurfaces
68 col
= layout
.column(align
=True)
71 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
72 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
74 col
.label(text
="Mesh of BSurface:")
75 col
.prop(bs
, "SURFSK_mesh", text
="")
76 if bs
.SURFSK_mesh
!= None:
77 try: mesh_object
= bs
.SURFSK_mesh
79 try: col
.prop(mesh_object
.data
.materials
[0], "diffuse_color")
82 shrinkwrap
= next(mod
for mod
in mesh_object
.modifiers
83 if mod
.type == 'SHRINKWRAP')
84 col
.prop(shrinkwrap
, "offset")
87 try: col
.prop(mesh_object
, "show_in_front")
89 try: col
.prop(bs
, "SURFSK_shade_smooth")
91 try: col
.prop(mesh_object
, "show_wire")
94 col
.label(text
="Guide strokes:")
95 col
.row().prop(bs
, "SURFSK_guide", expand
=True)
96 if bs
.SURFSK_guide
== 'GPencil':
97 col
.prop(bs
, "SURFSK_gpencil", text
="")
99 if bs
.SURFSK_guide
== 'Curve':
100 col
.prop(bs
, "SURFSK_curve", text
="")
104 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
105 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
108 if bs
.SURFSK_guide
== 'GPencil':
109 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
110 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
112 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
114 if bs
.SURFSK_guide
== 'Annotation':
115 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
117 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
119 if bs
.SURFSK_guide
== 'Curve':
120 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
123 col
.label(text
="Initial settings:")
124 col
.prop(bs
, "SURFSK_edges_U")
125 col
.prop(bs
, "SURFSK_edges_V")
126 col
.prop(bs
, "SURFSK_cyclic_cross")
127 col
.prop(bs
, "SURFSK_cyclic_follow")
128 col
.prop(bs
, "SURFSK_loops_on_strokes")
129 col
.prop(bs
, "SURFSK_automatic_join")
130 col
.prop(bs
, "SURFSK_keep_strokes")
132 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
133 bl_space_type
= 'VIEW_3D'
134 bl_region_type
= 'UI'
135 bl_context
= "curve_edit"
137 bl_label
= "Bsurfaces"
140 def poll(cls
, context
):
141 return context
.active_object
143 def draw(self
, context
):
146 col
= layout
.column(align
=True)
149 col
.operator("curve.surfsk_first_points", text
="Set First Points")
150 col
.operator("curve.switch_direction", text
="Switch Direction")
151 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
154 # ----------------------------
155 # Returns the type of strokes used
156 def get_strokes_type(context
):
157 strokes_type
= "NO_STROKES"
160 # Check if they are annotation
161 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
163 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
165 strokes_num
= len(strokes
)
168 strokes_type
= "GP_ANNOTATION"
170 strokes_type
= "NO_STROKES"
172 # Check if they are grease pencil
173 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
175 global global_gpencil_object
176 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
177 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
179 strokes_num
= len(strokes
)
182 strokes_type
= "GP_STROKES"
184 strokes_type
= "NO_STROKES"
186 # Check if they are curves, if there aren't grease pencil strokes
187 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
189 global global_curve_object
190 ob
= bpy
.data
.objects
[global_curve_object
]
191 if ob
.type == "CURVE":
192 strokes_type
= "EXTERNAL_CURVE"
193 strokes_num
= len(ob
.data
.splines
)
195 # Check if there is any non-bezier spline
196 for i
in range(len(ob
.data
.splines
)):
197 if ob
.data
.splines
[i
].type != "BEZIER":
198 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
202 strokes_type
= "EXTERNAL_NO_CURVE"
204 strokes_type
= "NO_STROKES"
206 # Check if they are mesh
208 global global_mesh_object
209 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
210 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
212 # Check if there is a single stroke without any selection in the object
213 if strokes_num
== 1 and total_vert_sel
== 0:
214 if strokes_type
== "EXTERNAL_CURVE":
215 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
216 elif strokes_type
== "GP_STROKES":
217 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
219 if strokes_num
== 0 and total_vert_sel
> 0:
220 strokes_type
= "SELECTION_ALONE"
226 # ----------------------------
227 # Surface generator operator
228 class MESH_OT_SURFSK_add_surface(Operator
):
229 bl_idname
= "mesh.surfsk_add_surface"
230 bl_label
= "Bsurfaces add surface"
231 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
232 bl_options
= {'REGISTER', 'UNDO'}
234 is_crosshatch
: BoolProperty(
237 is_fill_faces
: BoolProperty(
240 selection_U_exists
: BoolProperty(
243 selection_V_exists
: BoolProperty(
246 selection_U2_exists
: BoolProperty(
249 selection_V2_exists
: BoolProperty(
252 selection_V_is_closed
: BoolProperty(
255 selection_U_is_closed
: BoolProperty(
258 selection_V2_is_closed
: BoolProperty(
261 selection_U2_is_closed
: BoolProperty(
265 edges_U
: IntProperty(
267 description
="Number of face-loops crossing the strokes",
272 edges_V
: IntProperty(
274 description
="Number of face-loops following the strokes",
279 cyclic_cross
: BoolProperty(
281 description
="Make cyclic the face-loops crossing the strokes",
284 cyclic_follow
: BoolProperty(
285 name
="Cyclic Follow",
286 description
="Make cyclic the face-loops following the strokes",
289 loops_on_strokes
: BoolProperty(
290 name
="Loops on strokes",
291 description
="Make the loops match the paths of the strokes",
294 automatic_join
: BoolProperty(
295 name
="Automatic join",
296 description
="Join automatically vertices of either surfaces generated "
297 "by crosshatching, or from the borders of closed shapes",
300 join_stretch_factor
: FloatProperty(
302 description
="Amount of stretching or shrinking allowed for "
303 "edges when joining vertices automatically",
309 keep_strokes
: BoolProperty(
311 description
="Keeps the sketched strokes or curves after adding the surface",
314 strokes_type
: StringProperty()
315 initial_global_undo_state
: BoolProperty()
318 def draw(self
, context
):
320 col
= layout
.column(align
=True)
323 if not self
.is_fill_faces
:
325 if not self
.is_crosshatch
:
326 if not self
.selection_U_exists
:
327 col
.prop(self
, "edges_U")
330 if not self
.selection_V_exists
:
331 col
.prop(self
, "edges_V")
336 if not self
.selection_U_exists
:
338 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
339 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
341 col
.prop(self
, "cyclic_cross")
343 if not self
.selection_V_exists
:
345 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
346 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
348 col
.prop(self
, "cyclic_follow")
350 col
.prop(self
, "loops_on_strokes")
352 col
.prop(self
, "automatic_join")
354 if self
.automatic_join
:
358 col
.prop(self
, "join_stretch_factor")
360 col
.prop(self
, "keep_strokes")
362 # Get an ordered list of a chain of vertices
363 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
364 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
365 # Order selected vertices.
367 if closing_vert_idx
is not None:
368 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
370 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
371 prev_v
= first_vert_idx
375 edges_non_matched
= 0
376 for i
in all_selected_edges_idx
:
377 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
378 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
380 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
381 prev_v
= ob
.data
.edges
[i
].vertices
[1]
382 prev_ed
= ob
.data
.edges
[i
]
383 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
384 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
386 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
387 prev_v
= ob
.data
.edges
[i
].vertices
[0]
388 prev_ed
= ob
.data
.edges
[i
]
390 edges_non_matched
+= 1
392 if edges_non_matched
== len(all_selected_edges_idx
):
398 if closing_vert_idx
is not None:
399 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
401 if middle_vertex_idx
is not None:
402 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
403 verts_ordered
.reverse()
405 return tuple(verts_ordered
)
407 # Calculates length of a chain of points.
408 def get_chain_length(self
, object, verts_ordered
):
409 matrix
= object.matrix_world
412 edges_lengths_sum
= 0
413 for i
in range(0, len(verts_ordered
)):
415 prev_v_co
= matrix
@ verts_ordered
[i
].co
417 v_co
= matrix
@ verts_ordered
[i
].co
419 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
420 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
422 edges_lengths
.append(edge_length
)
423 edges_lengths_sum
+= edge_length
427 return edges_lengths
, edges_lengths_sum
429 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
430 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
431 edges_proportions
= []
434 for l
in edges_lengths
:
435 edges_proportions
.append(l
/ edges_lengths_sum
)
439 for _n
in range(0, fixed_edges_num
):
440 edges_proportions
.append(1 / fixed_edges_num
)
443 return edges_proportions
445 # Calculates the angle between two pairs of points in space
446 def orientation_difference(self
, points_A_co
, points_B_co
):
447 # each parameter should be a list with two elements,
448 # and each element should be a x,y,z coordinate
449 vec_A
= points_A_co
[0] - points_A_co
[1]
450 vec_B
= points_B_co
[0] - points_B_co
[1]
452 angle
= vec_A
.angle(vec_B
)
455 angle
= abs(angle
- pi
)
459 # Calculate the which vert of verts_idx list is the nearest one
460 # to the point_co coordinates, and the distance
461 def shortest_distance(self
, object, point_co
, verts_idx
):
462 matrix
= object.matrix_world
464 for i
in range(0, len(verts_idx
)):
465 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
468 nearest_vert_idx
= verts_idx
[i
]
473 nearest_vert_idx
= verts_idx
[i
]
476 return nearest_vert_idx
, shortest_dist
478 # Returns the index of the opposite vert tip in a chain, given a vert tip index
479 # as parameter, and a multidimentional list with all pairs of tips
480 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
481 opposite_vert_tip_idx
= None
482 for i
in range(0, len(all_chains_tips_idx
)):
483 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
484 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
485 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
486 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
488 return opposite_vert_tip_idx
490 # Simplifies a spline and returns the new points coordinates
491 def simplify_spline(self
, spline_coords
, segments_num
):
492 simplified_spline
= []
493 points_between_segments
= round(len(spline_coords
) / segments_num
)
495 simplified_spline
.append(spline_coords
[0])
496 for i
in range(1, segments_num
):
497 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
499 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
501 return simplified_spline
503 # Returns a list with the coords of the points distributed over the splines
504 # passed to this method according to the proportions parameter
505 def distribute_pts(self
, surface_splines
, proportions
):
507 # Calculate the length of each final surface spline
508 surface_splines_lengths
= []
509 surface_splines_parsed
= []
511 for sp_idx
in range(0, len(surface_splines
)):
512 # Calculate spline length
513 surface_splines_lengths
.append(0)
515 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
517 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
519 p
= surface_splines
[sp_idx
].bezier_points
[i
]
520 edge_length
= (prev_p
.co
- p
.co
).length
521 surface_splines_lengths
[sp_idx
] += edge_length
525 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
526 for sp_idx
in range(0, len(surface_splines
)):
527 surface_splines_parsed
.append([])
528 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
530 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
533 for prop_idx
in range(len(proportions
) - 1):
534 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
535 partial_segment_length
= 0
539 # if not it'll pass the p_idx as an index below and crash
540 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
541 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
542 new_dist
= (prev_p_co
- p_co
).length
544 # The new distance that could have the partial segment if
545 # it is still shorter than the target length
546 potential_segment_length
= partial_segment_length
+ new_dist
548 # If the potential is still shorter, keep adding
549 if potential_segment_length
< target_length
:
550 partial_segment_length
= potential_segment_length
555 # If the potential is longer than the target, calculate the target
556 # (a point between the last two points), and assign
557 elif potential_segment_length
> target_length
:
558 remaining_dist
= target_length
- partial_segment_length
559 vec
= p_co
- prev_p_co
561 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
563 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
565 partial_segment_length
+= remaining_dist
566 prev_p_co
= intermediate_co
570 # If the potential is equal to the target, assign
571 elif potential_segment_length
== target_length
:
572 surface_splines_parsed
[sp_idx
].append(p_co
)
580 # last point of the spline
581 surface_splines_parsed
[sp_idx
].append(
582 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
585 return surface_splines_parsed
587 # Counts the number of faces that belong to each edge
588 def edge_face_count(self
, ob
):
589 ed_keys_count_dict
= {}
591 for face
in ob
.data
.polygons
:
592 for ed_keys
in face
.edge_keys
:
593 if ed_keys
not in ed_keys_count_dict
:
594 ed_keys_count_dict
[ed_keys
] = 1
596 ed_keys_count_dict
[ed_keys
] += 1
599 for i
in range(len(ob
.data
.edges
)):
600 edge_face_count
.append(0)
602 for i
in range(len(ob
.data
.edges
)):
603 ed
= ob
.data
.edges
[i
]
608 if (v1
, v2
) in ed_keys_count_dict
:
609 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
610 elif (v2
, v1
) in ed_keys_count_dict
:
611 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
613 return edge_face_count
615 # Fills with faces all the selected vertices which form empty triangles or quads
616 def fill_with_faces(self
, object):
617 all_selected_verts_count
= self
.main_object_selected_verts_count
619 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
621 # Calculate average length of selected edges
622 all_selected_verts
= []
623 original_sel_edges_count
= 0
624 for ed
in object.data
.edges
:
625 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
627 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
628 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
630 original_sel_edges_count
+= 1
632 if not ed
.vertices
[0] in all_selected_verts
:
633 all_selected_verts
.append(ed
.vertices
[0])
635 if not ed
.vertices
[1] in all_selected_verts
:
636 all_selected_verts
.append(ed
.vertices
[1])
638 tuple(all_selected_verts
)
640 # Check if there is any edge selected. If not, interrupt the script
641 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
644 # Get all edges connected to selected verts
645 all_edges_around_sel_verts
= []
646 edges_connected_to_sel_verts
= {}
647 verts_connected_to_every_vert
= {}
648 for ed_idx
in range(len(object.data
.edges
)):
649 ed
= object.data
.edges
[ed_idx
]
652 if ed
.vertices
[0] in all_selected_verts
:
653 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
654 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
656 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
659 if ed
.vertices
[1] in all_selected_verts
:
660 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
661 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
663 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
666 if include_edge
is True:
667 all_edges_around_sel_verts
.append(ed_idx
)
669 # Get all connected verts to each vert
670 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
671 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
673 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
674 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
676 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
677 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
679 # Get all verts connected to faces
680 all_verts_part_of_faces
= []
681 all_edges_faces_count
= []
682 all_edges_faces_count
+= self
.edge_face_count(object)
684 # Get only the selected edges that have faces attached.
685 count_faces_of_edges_around_sel_verts
= {}
686 selected_verts_with_faces
= []
687 for ed_idx
in all_edges_around_sel_verts
:
688 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
690 if all_edges_faces_count
[ed_idx
] > 0:
691 ed
= object.data
.edges
[ed_idx
]
693 if not ed
.vertices
[0] in selected_verts_with_faces
:
694 selected_verts_with_faces
.append(ed
.vertices
[0])
696 if not ed
.vertices
[1] in selected_verts_with_faces
:
697 selected_verts_with_faces
.append(ed
.vertices
[1])
699 all_verts_part_of_faces
.append(ed
.vertices
[0])
700 all_verts_part_of_faces
.append(ed
.vertices
[1])
702 tuple(selected_verts_with_faces
)
704 # Discard unneeded verts from calculations
705 participating_verts
= []
707 for v_idx
in all_selected_verts
:
708 vert_has_edges_with_one_face
= False
710 # Check if the actual vert has at least one edge connected to only one face
711 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
712 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
713 vert_has_edges_with_one_face
= True
715 # If the vert has two or less edges connected and the vert is not part of any face.
716 # Or the vert is part of any face and at least one of
717 # the connected edges has only one face attached to it.
718 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
719 v_idx
not in all_verts_part_of_faces
) or \
720 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
721 (v_idx
in all_verts_part_of_faces
and
722 vert_has_edges_with_one_face
):
724 participating_verts
.append(v_idx
)
726 if v_idx
not in all_verts_part_of_faces
:
727 movable_verts
.append(v_idx
)
729 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
730 for mv_idx
in movable_verts
:
732 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
734 for actual_v_idx
in all_selected_verts
:
735 count_shared_neighbors
= 0
738 for mv_conn_v_idx
in mv_connected_verts
:
739 if mv_idx
!= actual_v_idx
:
740 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
741 mv_conn_v_idx
not in checked_verts
:
742 count_shared_neighbors
+= 1
743 checked_verts
.append(mv_conn_v_idx
)
745 if actual_v_idx
in mv_connected_verts
:
749 if count_shared_neighbors
== 2:
757 movable_verts
.remove(mv_idx
)
759 # Calculate merge distance for participating verts
760 shortest_edge_length
= None
761 for ed
in object.data
.edges
:
762 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
763 v1
= object.data
.vertices
[ed
.vertices
[0]]
764 v2
= object.data
.vertices
[ed
.vertices
[1]]
766 length
= (v1
.co
- v2
.co
).length
768 if shortest_edge_length
is None:
769 shortest_edge_length
= length
771 if length
< shortest_edge_length
:
772 shortest_edge_length
= length
774 if shortest_edge_length
is not None:
775 edges_merge_distance
= shortest_edge_length
* 0.5
777 edges_merge_distance
= 0
779 # Get together the verts near enough. They will be merged later
781 remaining_verts
+= participating_verts
782 for v1_idx
in participating_verts
:
783 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
785 coords_verts_to_merge
= {}
787 verts_to_merge
.append(v1_idx
)
789 v1_co
= object.data
.vertices
[v1_idx
].co
790 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
792 for v2_idx
in remaining_verts
:
794 v2_co
= object.data
.vertices
[v2_idx
].co
796 dist
= (v1_co
- v2_co
).length
798 if dist
<= edges_merge_distance
: # Add the verts which are near enough
799 verts_to_merge
.append(v2_idx
)
801 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
803 for vm_idx
in verts_to_merge
:
804 remaining_verts
.remove(vm_idx
)
806 if len(verts_to_merge
) > 1:
807 # Calculate middle point of the verts to merge.
811 movable_verts_to_merge_count
= 0
812 for i
in range(len(verts_to_merge
)):
813 if verts_to_merge
[i
] in movable_verts
:
814 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
820 movable_verts_to_merge_count
+= 1
823 sum_x_co
/ movable_verts_to_merge_count
,
824 sum_y_co
/ movable_verts_to_merge_count
,
825 sum_z_co
/ movable_verts_to_merge_count
828 # Check if any vert to be merged is not movable
830 are_verts_not_movable
= False
831 verts_not_movable
= []
832 for v_merge_idx
in verts_to_merge
:
833 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
834 are_verts_not_movable
= True
835 verts_not_movable
.append(v_merge_idx
)
837 if are_verts_not_movable
:
838 # Get the vert connected to faces, that is nearest to
839 # the middle point of the movable verts
841 for vcf_idx
in verts_not_movable
:
842 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
843 Vector(middle_point_co
)).length
)
845 if shortest_dist
is None:
847 nearest_vert_idx
= vcf_idx
849 if dist
< shortest_dist
:
851 nearest_vert_idx
= vcf_idx
853 coords
= object.data
.vertices
[nearest_vert_idx
].co
854 target_point_co
= [coords
[0], coords
[1], coords
[2]]
856 target_point_co
= middle_point_co
858 # Move verts to merge to the middle position
859 for v_merge_idx
in verts_to_merge
:
860 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
861 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
862 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
863 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
865 # Perform "Remove Doubles" to weld all the disconnected verts
866 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
867 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
869 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
871 # Get all the definitive selected edges, after weldding
873 edges_per_vert
= {} # Number of faces of each selected edge
874 for ed
in object.data
.edges
:
875 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
876 selected_edges
.append(ed
.index
)
878 # Save all the edges that belong to each vertex.
879 if not ed
.vertices
[0] in edges_per_vert
:
880 edges_per_vert
[ed
.vertices
[0]] = []
882 if not ed
.vertices
[1] in edges_per_vert
:
883 edges_per_vert
[ed
.vertices
[1]] = []
885 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
886 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
888 # Check if all the edges connected to each vert have two faces attached to them.
889 # To discard them later and make calculations faster
891 a
+= self
.edge_face_count(object)
893 verts_surrounded_by_faces
= {}
894 for v_idx
in edges_per_vert
:
895 edges_with_two_faces_count
= 0
897 for ed_idx
in edges_per_vert
[v_idx
]:
899 edges_with_two_faces_count
+= 1
901 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
902 verts_surrounded_by_faces
[v_idx
] = True
904 verts_surrounded_by_faces
[v_idx
] = False
906 # Get all the selected vertices
907 selected_verts_idx
= []
908 for v
in object.data
.vertices
:
910 selected_verts_idx
.append(v
.index
)
912 # Get all the faces of the object
913 all_object_faces_verts_idx
= []
914 for face
in object.data
.polygons
:
916 face_verts
.append(face
.vertices
[0])
917 face_verts
.append(face
.vertices
[1])
918 face_verts
.append(face
.vertices
[2])
920 if len(face
.vertices
) == 4:
921 face_verts
.append(face
.vertices
[3])
923 all_object_faces_verts_idx
.append(face_verts
)
925 # Deselect all vertices
926 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
927 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
928 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
930 # Make a dictionary with the verts related to each vert
931 related_key_verts
= {}
932 for ed_idx
in selected_edges
:
933 ed
= object.data
.edges
[ed_idx
]
935 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
936 if not ed
.vertices
[0] in related_key_verts
:
937 related_key_verts
[ed
.vertices
[0]] = []
939 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
940 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
942 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
943 if not ed
.vertices
[1] in related_key_verts
:
944 related_key_verts
[ed
.vertices
[1]] = []
946 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
947 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
949 # Get groups of verts forming each face
951 for v1
in related_key_verts
: # verts-1 ....
952 for v2
in related_key_verts
: # verts-2
954 related_verts_in_common
= []
957 for rel_v1
in related_key_verts
[v1
]:
958 # Check if related verts of verts-1 are related verts of verts-2
959 if rel_v1
in related_key_verts
[v2
]:
960 related_verts_in_common
.append(rel_v1
)
962 if v2
in related_key_verts
[v1
]:
965 if v1
in related_key_verts
[v2
]:
968 repeated_face
= False
969 # If two verts have two related verts in common, they form a quad
970 if len(related_verts_in_common
) == 2:
971 # Check if the face is already saved
972 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
974 for f_verts
in all_faces_to_check_idx
:
977 if len(f_verts
) == 4:
982 if related_verts_in_common
[0] in f_verts
:
984 if related_verts_in_common
[1] in f_verts
:
987 if repeated_verts
== len(f_verts
):
991 if not repeated_face
:
992 faces_verts_idx
.append(
993 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
996 # If Two verts have one related vert in common and
997 # they are related to each other, they form a triangle
998 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
999 # Check if the face is already saved.
1000 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1002 for f_verts
in all_faces_to_check_idx
:
1005 if len(f_verts
) == 3:
1010 if related_verts_in_common
[0] in f_verts
:
1013 if repeated_verts
== len(f_verts
):
1014 repeated_face
= True
1017 if not repeated_face
:
1018 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1020 # Keep only the faces that don't overlap by ignoring quads
1021 # that overlap with two adjacent triangles
1022 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1023 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1024 for i
in range(len(faces_verts_idx
)):
1025 for t
in range(len(all_faces_to_check_idx
)):
1029 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1030 for v_idx
in all_faces_to_check_idx
[t
]:
1031 if v_idx
in faces_verts_idx
[i
]:
1032 verts_in_common
+= 1
1033 # If it doesn't have all it's vertices repeated in the other face
1034 if verts_in_common
== 3:
1035 if i
not in faces_to_not_include_idx
:
1036 faces_to_not_include_idx
.append(i
)
1038 # Build faces discarding the ones in faces_to_not_include
1043 num_faces_created
= 0
1044 for i
in range(len(faces_verts_idx
)):
1045 if i
not in faces_to_not_include_idx
:
1046 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1048 num_faces_created
+= 1
1053 for v_idx
in selected_verts_idx
:
1054 self
.main_object
.data
.vertices
[v_idx
].select
= True
1056 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1057 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1058 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1062 return num_faces_created
1064 # Crosshatch skinning
1065 def crosshatch_surface_invoke(self
, ob_original_splines
):
1066 self
.is_crosshatch
= False
1067 self
.crosshatch_merge_distance
= 0
1069 objects_to_delete
= [] # duplicated strokes to be deleted.
1071 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1072 # (without this the surface verts merging with the main object doesn't work well)
1073 self
.modifiers_prev_viewport_state
= []
1074 if len(self
.main_object
.modifiers
) > 0:
1075 for m_idx
in range(len(self
.main_object
.modifiers
)):
1076 self
.modifiers_prev_viewport_state
.append(
1077 self
.main_object
.modifiers
[m_idx
].show_viewport
1079 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1081 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1082 ob_original_splines
.select_set(True)
1083 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1085 if len(ob_original_splines
.data
.splines
) >= 2:
1086 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1087 ob_splines
= bpy
.context
.object
1088 ob_splines
.name
= "SURFSKIO_NE_STR"
1090 # Get estimative merge distance (sum up the distances from the first point to
1091 # all other points, then average them and then divide them)
1092 first_point_dist_sum
= 0
1095 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1096 for i
in range(len(ob_splines
.data
.splines
)):
1097 sp
= ob_splines
.data
.splines
[i
]
1099 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1100 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1102 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1103 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1105 first_point_dist_sum
+= first_dist
+ second_dist
1109 shortest_dist
= first_dist
1110 elif second_dist
!= 0:
1111 shortest_dist
= second_dist
1113 if shortest_dist
> first_dist
and first_dist
!= 0:
1114 shortest_dist
= first_dist
1116 if shortest_dist
> second_dist
and second_dist
!= 0:
1117 shortest_dist
= second_dist
1119 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1121 # Recalculation of merge distance
1123 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1125 ob_calc_merge_dist
= bpy
.context
.object
1126 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1128 objects_to_delete
.append(ob_calc_merge_dist
)
1130 # Smooth out strokes a little to improve crosshatch detection
1131 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1132 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1135 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1137 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1138 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1140 # Convert curves into mesh
1141 ob_calc_merge_dist
.data
.resolution_u
= 12
1142 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1144 # Find "intersection-nodes"
1145 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1146 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1147 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1148 threshold
=self
.crosshatch_merge_distance
)
1149 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1150 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1152 # Remove verts with less than three edges
1153 verts_edges_count
= {}
1154 for ed
in ob_calc_merge_dist
.data
.edges
:
1157 if v
[0] not in verts_edges_count
:
1158 verts_edges_count
[v
[0]] = 0
1160 if v
[1] not in verts_edges_count
:
1161 verts_edges_count
[v
[1]] = 0
1163 verts_edges_count
[v
[0]] += 1
1164 verts_edges_count
[v
[1]] += 1
1166 nodes_verts_coords
= []
1167 for v_idx
in verts_edges_count
:
1168 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1170 if verts_edges_count
[v_idx
] < 3:
1174 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1175 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1176 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1178 # Remove doubles to discard very near verts from calculations of distance
1179 bpy
.ops
.mesh
.remove_doubles(
1180 'INVOKE_REGION_WIN',
1181 threshold
=self
.crosshatch_merge_distance
* 4.0
1183 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1185 # Get all coords of the resulting nodes
1186 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1187 v
in ob_calc_merge_dist
.data
.vertices
]
1189 # Check if the strokes are a crosshatch
1190 if len(nodes_verts_coords
) >= 3:
1191 self
.is_crosshatch
= True
1193 shortest_dist
= None
1194 for co_1
in nodes_verts_coords
:
1195 for co_2
in nodes_verts_coords
:
1197 dist
= (Vector(co_1
) - Vector(co_2
)).length
1199 if shortest_dist
is not None:
1200 if dist
< shortest_dist
:
1201 shortest_dist
= dist
1203 shortest_dist
= dist
1205 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1207 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1208 ob_splines
.select_set(True)
1209 bpy
.context
.view_layer
.objects
.active
= ob_splines
1211 # Deselect all points
1212 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1213 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1214 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1216 # Smooth splines in a localized way, to eliminate "saw-teeth"
1217 # like shapes when there are many points
1218 for sp
in ob_splines
.data
.splines
:
1221 angle_limit
= 2 # Degrees
1222 for t
in range(len(sp
.bezier_points
)):
1223 # Because on each iteration it checks the "next two points"
1224 # of the actual. This way it doesn't go out of range
1225 if t
<= len(sp
.bezier_points
) - 3:
1226 p1
= sp
.bezier_points
[t
]
1227 p2
= sp
.bezier_points
[t
+ 1]
1228 p3
= sp
.bezier_points
[t
+ 2]
1230 vec_1
= p1
.co
- p2
.co
1231 vec_2
= p2
.co
- p3
.co
1233 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1234 angle
= vec_1
.angle(vec_2
)
1235 angle_sum
+= degrees(angle
)
1237 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1238 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1239 p1
.select_control_point
= True
1240 p1
.select_left_handle
= True
1241 p1
.select_right_handle
= True
1243 p2
.select_control_point
= True
1244 p2
.select_left_handle
= True
1245 p2
.select_right_handle
= True
1247 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1248 p3
.select_control_point
= True
1249 p3
.select_left_handle
= True
1250 p3
.select_right_handle
= True
1254 sp
.bezier_points
[0].select_control_point
= False
1255 sp
.bezier_points
[0].select_left_handle
= False
1256 sp
.bezier_points
[0].select_right_handle
= False
1258 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1259 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1260 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1262 # Smooth out strokes a little to improve crosshatch detection
1263 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1266 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1268 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1269 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1271 # Simplify the splines
1272 for sp
in ob_splines
.data
.splines
:
1275 sp
.bezier_points
[0].select_control_point
= True
1276 sp
.bezier_points
[0].select_left_handle
= True
1277 sp
.bezier_points
[0].select_right_handle
= True
1279 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1280 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1281 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1283 angle_limit
= 15 # Degrees
1284 for t
in range(len(sp
.bezier_points
)):
1285 # Because on each iteration it checks the "next two points"
1286 # of the actual. This way it doesn't go out of range
1287 if t
<= len(sp
.bezier_points
) - 3:
1288 p1
= sp
.bezier_points
[t
]
1289 p2
= sp
.bezier_points
[t
+ 1]
1290 p3
= sp
.bezier_points
[t
+ 2]
1292 vec_1
= p1
.co
- p2
.co
1293 vec_2
= p2
.co
- p3
.co
1295 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1296 angle
= vec_1
.angle(vec_2
)
1297 angle_sum
+= degrees(angle
)
1298 # If sum of angles is grater than the limit
1299 if angle_sum
>= angle_limit
:
1300 p1
.select_control_point
= True
1301 p1
.select_left_handle
= True
1302 p1
.select_right_handle
= True
1304 p2
.select_control_point
= True
1305 p2
.select_left_handle
= True
1306 p2
.select_right_handle
= True
1308 p3
.select_control_point
= True
1309 p3
.select_left_handle
= True
1310 p3
.select_right_handle
= True
1314 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1315 bpy
.ops
.curve
.select_all(action
='INVERT')
1317 bpy
.ops
.curve
.delete(type='VERT')
1318 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1320 objects_to_delete
.append(ob_splines
)
1322 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1323 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1324 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1326 # Check if the strokes are a crosshatch
1327 if self
.is_crosshatch
:
1328 all_points_coords
= []
1329 for i
in range(len(ob_splines
.data
.splines
)):
1330 all_points_coords
.append([])
1332 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1333 x
, y
, z
in [bp
.co
for
1334 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1336 all_intersections
= []
1337 checked_splines
= []
1338 for i
in range(len(all_points_coords
)):
1340 for t
in range(len(all_points_coords
[i
]) - 1):
1341 bp1_co
= all_points_coords
[i
][t
]
1342 bp2_co
= all_points_coords
[i
][t
+ 1]
1344 for i2
in range(len(all_points_coords
)):
1345 if i
!= i2
and i2
not in checked_splines
:
1346 for t2
in range(len(all_points_coords
[i2
]) - 1):
1347 bp3_co
= all_points_coords
[i2
][t2
]
1348 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1350 intersec_coords
= intersect_line_line(
1351 bp1_co
, bp2_co
, bp3_co
, bp4_co
1353 if intersec_coords
is not None:
1354 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1356 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1357 _temp_co
, percent1
= intersect_point_line(
1358 intersec_coords
[0], bp1_co
, bp2_co
1360 if (percent1
>= -0.02 and percent1
<= 1.02):
1361 _temp_co
, percent2
= intersect_point_line(
1362 intersec_coords
[1], bp3_co
, bp4_co
1364 if (percent2
>= -0.02 and percent2
<= 1.02):
1365 # Format: spline index, first point index from
1366 # corresponding segment, percentage from first point of
1367 # actual segment, coords of intersection point
1368 all_intersections
.append(
1370 ob_splines
.matrix_world
@ intersec_coords
[0])
1372 all_intersections
.append(
1374 ob_splines
.matrix_world
@ intersec_coords
[1])
1377 checked_splines
.append(i
)
1378 # Sort list by spline, then by corresponding first point index of segment,
1379 # and then by percentage from first point of segment: elements 0 and 1 respectively
1380 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1382 self
.crosshatch_strokes_coords
= {}
1383 for i
in range(len(all_intersections
)):
1384 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1385 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1387 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1388 all_intersections
[i
][3]
1389 ) # Save intersection coords
1391 self
.is_crosshatch
= False
1393 # Delete all duplicates
1394 with bpy
.context
.temp_override(selected_objects
=objects_to_delete
):
1395 bpy
.ops
.object.delete()
1397 # If the main object has modifiers, turn their "viewport view status" to
1398 # what it was before the forced deactivation above
1399 if len(self
.main_object
.modifiers
) > 0:
1400 for m_idx
in range(len(self
.main_object
.modifiers
)):
1401 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1407 # Part of the Crosshatch process that is repeated when the operator is tweaked
1408 def crosshatch_surface_execute(self
, context
):
1409 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1410 # (without this the surface verts merging with the main object doesn't work well)
1411 self
.modifiers_prev_viewport_state
= []
1412 if len(self
.main_object
.modifiers
) > 0:
1413 for m_idx
in range(len(self
.main_object
.modifiers
)):
1414 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1416 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1418 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1420 me_name
= "SURFSKIO_STK_TMP"
1421 me
= bpy
.data
.meshes
.new(me_name
)
1423 all_verts_coords
= []
1425 for st_idx
in self
.crosshatch_strokes_coords
:
1426 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1427 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1429 all_verts_coords
.append(coords
)
1432 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1434 me
.from_pydata(all_verts_coords
, all_edges
, [])
1435 ob
= object_utils
.object_data_add(context
, me
)
1436 ob
.location
= (0.0, 0.0, 0.0)
1437 ob
.rotation_euler
= (0.0, 0.0, 0.0)
1438 ob
.scale
= (1.0, 1.0, 1.0)
1440 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1442 bpy
.context
.view_layer
.objects
.active
= ob
1444 # Get together each vert and its nearest, to the middle position
1445 verts
= ob
.data
.vertices
1447 for i
in range(len(verts
)):
1448 shortest_dist
= None
1450 if i
not in checked_verts
:
1451 for t
in range(len(verts
)):
1452 if i
!= t
and t
not in checked_verts
:
1453 dist
= (verts
[i
].co
- verts
[t
].co
).length
1455 if shortest_dist
is not None:
1456 if dist
< shortest_dist
:
1457 shortest_dist
= dist
1460 shortest_dist
= dist
1463 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1465 verts
[i
].co
= middle_location
1466 verts
[nearest_vert
].co
= middle_location
1468 checked_verts
.append(i
)
1469 checked_verts
.append(nearest_vert
)
1471 # Calculate average length between all the generated edges
1472 ob
= bpy
.context
.object
1474 for ed
in ob
.data
.edges
:
1475 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1476 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1478 lengths_sum
+= (v1
.co
- v2
.co
).length
1480 edges_count
= len(ob
.data
.edges
)
1481 # possible division by zero here
1482 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1484 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1485 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1486 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1487 threshold
=average_edge_length
/ 15.0)
1488 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1490 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1492 # Make a dictionary with the verts related to each vert
1493 related_key_verts
= {}
1494 for ed
in final_points_ob
.data
.edges
:
1495 if not ed
.vertices
[0] in related_key_verts
:
1496 related_key_verts
[ed
.vertices
[0]] = []
1498 if not ed
.vertices
[1] in related_key_verts
:
1499 related_key_verts
[ed
.vertices
[1]] = []
1501 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1502 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1504 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1505 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1507 # Get groups of verts forming each face
1508 faces_verts_idx
= []
1509 for v1
in related_key_verts
: # verts-1 ....
1510 for v2
in related_key_verts
: # verts-2
1512 related_verts_in_common
= []
1513 v2_in_rel_v1
= False
1514 v1_in_rel_v2
= False
1515 for rel_v1
in related_key_verts
[v1
]:
1516 # Check if related verts of verts-1 are related verts of verts-2
1517 if rel_v1
in related_key_verts
[v2
]:
1518 related_verts_in_common
.append(rel_v1
)
1520 if v2
in related_key_verts
[v1
]:
1523 if v1
in related_key_verts
[v2
]:
1526 repeated_face
= False
1527 # If two verts have two related verts in common, they form a quad
1528 if len(related_verts_in_common
) == 2:
1529 # Check if the face is already saved
1530 for f_verts
in faces_verts_idx
:
1533 if len(f_verts
) == 4:
1538 if related_verts_in_common
[0] in f_verts
:
1540 if related_verts_in_common
[1] in f_verts
:
1543 if repeated_verts
== len(f_verts
):
1544 repeated_face
= True
1547 if not repeated_face
:
1548 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1549 v2
, related_verts_in_common
[1]])
1551 # If Two verts have one related vert in common and they are
1552 # related to each other, they form a triangle
1553 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1554 # Check if the face is already saved.
1555 for f_verts
in faces_verts_idx
:
1558 if len(f_verts
) == 3:
1563 if related_verts_in_common
[0] in f_verts
:
1566 if repeated_verts
== len(f_verts
):
1567 repeated_face
= True
1570 if not repeated_face
:
1571 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1573 # Keep only the faces that don't overlap by ignoring
1574 # quads that overlap with two adjacent triangles
1575 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1576 for i
in range(len(faces_verts_idx
)):
1577 for t
in range(len(faces_verts_idx
)):
1581 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1582 for v_idx
in faces_verts_idx
[t
]:
1583 if v_idx
in faces_verts_idx
[i
]:
1584 verts_in_common
+= 1
1585 # If it doesn't have all it's vertices repeated in the other face
1586 if verts_in_common
== 3:
1587 if i
not in faces_to_not_include_idx
:
1588 faces_to_not_include_idx
.append(i
)
1591 all_surface_verts_co
= []
1592 for i
in range(len(final_points_ob
.data
.vertices
)):
1593 coords
= final_points_ob
.data
.vertices
[i
].co
1594 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1596 # Verts of each face.
1597 all_surface_faces
= []
1598 for i
in range(len(faces_verts_idx
)):
1599 if i
not in faces_to_not_include_idx
:
1601 for v_idx
in faces_verts_idx
[i
]:
1604 all_surface_faces
.append(face
)
1607 surf_me_name
= "SURFSKIO_surface"
1608 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1609 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1610 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1611 ob_surface
.location
= (0.0, 0.0, 0.0)
1612 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
1613 ob_surface
.scale
= (1.0, 1.0, 1.0)
1615 # Delete final points temporal object
1616 with bpy
.context
.temp_override(selected_objects
=[final_points_ob
]):
1617 bpy
.ops
.object.delete()
1619 # Delete isolated verts if there are any
1620 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1621 ob_surface
.select_set(True)
1622 bpy
.context
.view_layer
.objects
.active
= ob_surface
1624 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1625 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1626 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1627 bpy
.ops
.mesh
.delete()
1628 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1630 # Join crosshatch results with original mesh
1632 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1633 edges_length_sum
= 0
1634 for ed
in ob_surface
.data
.edges
:
1635 edges_length_sum
+= (
1636 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1637 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1640 # Make dictionary with all the verts connected to each vert, on the new surface object.
1641 surface_connected_verts
= {}
1642 for ed
in ob_surface
.data
.edges
:
1643 if not ed
.vertices
[0] in surface_connected_verts
:
1644 surface_connected_verts
[ed
.vertices
[0]] = []
1646 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1648 if ed
.vertices
[1] not in surface_connected_verts
:
1649 surface_connected_verts
[ed
.vertices
[1]] = []
1651 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1653 # Duplicate the new surface object, and use shrinkwrap to
1654 # calculate later the nearest verts to the main object
1655 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1656 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1657 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1659 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1661 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1663 shrinkwrap_modifier
= context
.object.modifiers
.new("", 'SHRINKWRAP')
1664 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1665 shrinkwrap_modifier
.target
= self
.main_object
1667 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap_modifier
.name
)
1669 # Make list with verts of original mesh as index and coords as value
1670 main_object_verts_coords
= []
1671 for v
in self
.main_object
.data
.vertices
:
1672 coords
= self
.main_object
.matrix_world
@ v
.co
1674 # To avoid problems when taking "-0.00" as a different value as "0.00"
1675 for c
in range(len(coords
)):
1676 if "%.3f" % coords
[c
] == "-0.00":
1679 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1681 tuple(main_object_verts_coords
)
1683 # Determine which verts will be merged, snap them to the nearest verts
1684 # on the original verts, and get them selected
1685 crosshatch_verts_to_merge
= []
1686 if self
.automatic_join
:
1687 for i
in range(len(ob_surface
.data
.vertices
)-1):
1688 # Calculate the distance from each of the connected verts to the actual vert,
1689 # and compare it with the distance they would have if joined.
1690 # If they don't change much, that vert can be joined
1691 merge_actual_vert
= True
1693 if len(surface_connected_verts
[i
]) < 4:
1694 for c_v_idx
in surface_connected_verts
[i
]:
1695 points_original
= []
1696 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1697 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1700 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1701 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1703 vec_A
= points_original
[0] - points_original
[1]
1704 vec_B
= points_target
[0] - points_target
[1]
1706 dist_A
= (points_original
[0] - points_original
[1]).length
1707 dist_B
= (points_target
[0] - points_target
[1]).length
1710 points_original
[0] == points_original
[1] or
1711 points_target
[0] == points_target
[1]
1712 ): # If any vector's length is zero
1714 angle
= vec_A
.angle(vec_B
) / pi
1718 # Set a range of acceptable variation in the connected edges
1719 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1720 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1721 angle
>= 0.15 * self
.join_stretch_factor
:
1723 merge_actual_vert
= False
1726 merge_actual_vert
= False
1728 self
.report({'WARNING'},
1729 "Crosshatch set incorrectly")
1731 if merge_actual_vert
:
1732 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1733 # To avoid problems when taking "-0.000" as a different value as "0.00"
1734 for c
in range(len(coords
)):
1735 if "%.3f" % coords
[c
] == "-0.00":
1738 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1740 if comparison_coords
in main_object_verts_coords
:
1741 # Get the index of the vert with those coords in the main object
1742 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1744 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1745 self
.main_object_selected_verts_count
== 0:
1747 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1748 ob_surface
.data
.vertices
[i
].select
= True
1749 crosshatch_verts_to_merge
.append(i
)
1751 # Make sure the vert in the main object is selected,
1752 # in case it wasn't selected and the "join crosshatch" option is active
1753 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1755 # Delete duplicated object
1756 with bpy
.context
.temp_override(selected_objects
=[final_ob_duplicate
]):
1757 bpy
.ops
.object.delete()
1759 # Join crosshatched surface and main object
1760 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1761 ob_surface
.select_set(True)
1762 self
.main_object
.select_set(True)
1763 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1765 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1767 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1768 # Perform Remove doubles to merge verts
1769 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1770 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1772 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1774 # If the main object has modifiers, turn their "viewport view status"
1775 # to what it was before the forced deactivation above
1776 if len(self
.main_object
.modifiers
) > 0:
1777 for m_idx
in range(len(self
.main_object
.modifiers
)):
1778 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1784 def rectangular_surface(self
, context
):
1786 all_selected_edges_idx
= []
1787 all_selected_verts
= []
1789 for ed
in self
.main_object
.data
.edges
:
1791 all_selected_edges_idx
.append(ed
.index
)
1794 if not ed
.vertices
[0] in all_selected_verts
:
1795 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1796 if not ed
.vertices
[1] in all_selected_verts
:
1797 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1799 # All verts (both from each edge) to determine later
1800 # which are at the tips (those not repeated twice)
1801 all_verts_idx
.append(ed
.vertices
[0])
1802 all_verts_idx
.append(ed
.vertices
[1])
1804 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1805 all_chains_tips_idx
= []
1806 for v_idx
in all_verts_idx
:
1807 if all_verts_idx
.count(v_idx
) < 2:
1808 all_chains_tips_idx
.append(v_idx
)
1810 edges_connected_to_tips
= []
1811 for ed
in self
.main_object
.data
.edges
:
1812 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1813 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1815 edges_connected_to_tips
.append(ed
)
1817 # Check closed selections
1818 # List with groups of three verts, where the first element of the pair is
1819 # the unselected vert of a closed selection and the other two elements are the
1820 # selected neighbor verts (it will be useful to determine which selection chain
1821 # the unselected vert belongs to, and determine the "middle-vertex")
1822 single_unselected_verts_and_neighbors
= []
1824 # To identify a "closed" selection (a selection that is a closed chain except
1825 # for one vertex) find the vertex in common that have the edges connected to tips.
1826 # If there is a vertex in common, that one is the unselected vert that closes
1827 # the selection or is a "middle-vertex"
1828 single_unselected_verts
= []
1829 for ed
in edges_connected_to_tips
:
1830 for ed_b
in edges_connected_to_tips
:
1832 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1833 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1834 ed
.vertices
[0] not in single_unselected_verts
:
1836 # The second element is one of the tips of the selected
1837 # vertices of the closed selection
1838 single_unselected_verts_and_neighbors
.append(
1839 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1841 single_unselected_verts
.append(ed
.vertices
[0])
1843 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1844 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1845 ed
.vertices
[0] not in single_unselected_verts
:
1847 single_unselected_verts_and_neighbors
.append(
1848 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1850 single_unselected_verts
.append(ed
.vertices
[0])
1852 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1853 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1854 ed
.vertices
[1] not in single_unselected_verts
:
1856 single_unselected_verts_and_neighbors
.append(
1857 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1859 single_unselected_verts
.append(ed
.vertices
[1])
1861 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1862 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1863 ed
.vertices
[1] not in single_unselected_verts
:
1865 single_unselected_verts_and_neighbors
.append(
1866 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1868 single_unselected_verts
.append(ed
.vertices
[1])
1871 middle_vertex_idx
= None
1872 tips_to_discard_idx
= []
1874 # Check if there is a "middle-vertex", and get its index
1875 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1876 actual_chain_verts
= self
.get_ordered_verts(
1877 self
.main_object
, all_selected_edges_idx
,
1878 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1882 if single_unselected_verts_and_neighbors
[i
][2] != \
1883 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1885 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1886 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1887 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1889 # List with pairs of verts that belong to the tips of each selection chain (row)
1890 verts_tips_same_chain_idx
= []
1891 if len(all_chains_tips_idx
) >= 2:
1893 for i
in range(0, len(all_chains_tips_idx
)):
1894 if all_chains_tips_idx
[i
] not in checked_v
:
1895 v_chain
= self
.get_ordered_verts(
1896 self
.main_object
, all_selected_edges_idx
,
1897 all_verts_idx
, all_chains_tips_idx
[i
],
1898 middle_vertex_idx
, None
1901 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1903 checked_v
.append(v_chain
[0].index
)
1904 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1906 # Selection tips (vertices).
1907 verts_tips_parsed_idx
= []
1908 if len(all_chains_tips_idx
) >= 2:
1909 for spec_v_idx
in all_chains_tips_idx
:
1910 if (spec_v_idx
not in tips_to_discard_idx
):
1911 verts_tips_parsed_idx
.append(spec_v_idx
)
1913 # Identify the type of selection made by the user
1914 if middle_vertex_idx
is not None:
1915 # If there are 4 tips (two selection chains), and
1916 # there is only one single unselected vert (the middle vert)
1917 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1918 selection_type
= "TWO_CONNECTED"
1920 # The type of the selection was not identified, the script stops.
1921 self
.report({'WARNING'}, "The selection isn't valid.")
1923 self
.stopping_errors
= True
1927 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1928 selection_type
= "SINGLE"
1929 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1930 selection_type
= "TWO_NOT_CONNECTED"
1931 elif len(all_chains_tips_idx
) == 0:
1932 if len(self
.main_splines
.data
.splines
) > 1:
1933 selection_type
= "NO_SELECTION"
1935 # If the selection was not identified and there is only one stroke,
1936 # there's no possibility to build a surface, so the script is interrupted
1937 self
.report({'WARNING'}, "The selection isn't valid.")
1939 self
.stopping_errors
= True
1943 # The type of the selection was not identified, the script stops
1944 self
.report({'WARNING'}, "The selection isn't valid.")
1946 self
.stopping_errors
= True
1950 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1951 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1952 self
.report({'WARNING'},
1953 "At least two strokes are needed when there are two not connected selections")
1955 self
.stopping_errors
= True
1959 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1961 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1962 self
.main_splines
.select_set(True)
1963 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1965 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1966 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1967 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1968 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1969 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1970 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1971 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1972 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1973 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1975 self
.selection_U_exists
= False
1976 self
.selection_U2_exists
= False
1977 self
.selection_V_exists
= False
1978 self
.selection_V2_exists
= False
1980 self
.selection_U_is_closed
= False
1981 self
.selection_U2_is_closed
= False
1982 self
.selection_V_is_closed
= False
1983 self
.selection_V2_is_closed
= False
1985 # Define what vertices are at the tips of each selection and are not the middle-vertex
1986 if selection_type
== "TWO_CONNECTED":
1987 self
.selection_U_exists
= True
1988 self
.selection_V_exists
= True
1990 closing_vert_U_idx
= None
1991 closing_vert_V_idx
= None
1992 closing_vert_U2_idx
= None
1993 closing_vert_V2_idx
= None
1995 # Determine which selection is Selection-U and which is Selection-V
1998 points_first_stroke_tips
= []
2001 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
2004 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2007 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2010 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2012 points_first_stroke_tips
.append(
2013 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2015 points_first_stroke_tips
.append(
2016 self
.main_splines
.data
.splines
[0].bezier_points
[
2017 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2021 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2022 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2024 if angle_A
< angle_B
:
2025 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2026 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2028 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2029 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2031 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2032 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2033 last_sketched_point_first_stroke_co
= \
2034 self
.main_splines
.data
.splines
[0].bezier_points
[
2035 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2037 first_sketched_point_last_stroke_co
= \
2038 self
.main_splines
.data
.splines
[
2039 len(self
.main_splines
.data
.splines
) - 1
2040 ].bezier_points
[0].co
2041 if len(self
.main_splines
.data
.splines
) > 1:
2042 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2043 last_sketched_point_second_stroke_co
= \
2044 self
.main_splines
.data
.splines
[1].bezier_points
[
2045 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2048 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2049 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2050 single_unselected_neighbors
.append(verts_neig_idx
[1])
2051 single_unselected_neighbors
.append(verts_neig_idx
[2])
2053 all_chains_tips_and_middle_vert
= []
2054 for v_idx
in all_chains_tips_idx
:
2055 if v_idx
not in single_unselected_neighbors
:
2056 all_chains_tips_and_middle_vert
.append(v_idx
)
2058 all_chains_tips_and_middle_vert
+= single_unselected_verts
2060 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2062 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2063 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2064 self
.shortest_distance(
2066 first_sketched_point_first_stroke_co
,
2067 all_chains_tips_and_middle_vert
2069 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2070 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2071 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2073 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2075 nearest_tip_to_first_st_first_pt_idx
,
2076 verts_tips_same_chain_idx
2078 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2079 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2080 self
.shortest_distance(
2082 last_sketched_point_first_stroke_co
,
2083 all_chains_tips_and_middle_vert
2085 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2086 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2087 self
.shortest_distance(
2089 first_sketched_point_last_stroke_co
,
2090 all_chains_tips_and_middle_vert
2092 if len(self
.main_splines
.data
.splines
) > 1:
2093 # The selected vertex nearest to the first point of the second sketched stroke
2094 # (This will be useful to determine the direction of the closed
2095 # selection V when extruding along strokes)
2096 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2097 self
.shortest_distance(
2099 first_sketched_point_second_stroke_co
,
2102 # The selected vertex nearest to the first point of the second sketched stroke
2103 # (This will be useful to determine the direction of the closed
2104 # selection V2 when extruding along strokes)
2105 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2106 self
.shortest_distance(
2108 last_sketched_point_second_stroke_co
,
2111 # Determine if the single selection will be treated as U or as V
2113 for i
in all_selected_edges_idx
:
2115 (self
.main_object
.matrix_world
@
2116 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2117 (self
.main_object
.matrix_world
@
2118 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2121 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2123 # Get shortest distance from the first point of the last stroke to any participating vertex
2124 _temp_idx
, shortest_distance_to_last_stroke
= \
2125 self
.shortest_distance(
2127 first_sketched_point_last_stroke_co
,
2128 all_participating_verts
2130 # If the beginning of the first stroke is near enough, and its orientation
2131 # difference with the first edge of the nearest selection chain is not too high,
2132 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2133 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2134 shortest_distance_to_last_stroke
< average_edge_length
and \
2135 len(self
.main_splines
.data
.splines
) > 1:
2137 self
.selection_U_exists
= False
2138 self
.selection_V_exists
= True
2139 # If the first selection is not closed
2140 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2141 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2142 self
.selection_V_is_closed
= False
2143 closing_vert_U_idx
= None
2144 closing_vert_U2_idx
= None
2145 closing_vert_V_idx
= None
2146 closing_vert_V2_idx
= None
2148 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2150 if selection_type
== "TWO_NOT_CONNECTED":
2151 self
.selection_V2_exists
= True
2153 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2155 self
.selection_V_is_closed
= True
2156 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2158 # Get the neighbors of the first (unselected) vert of the closed selection U.
2160 for verts
in single_unselected_verts_and_neighbors
:
2161 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2162 vert_neighbors
.append(verts
[1])
2163 vert_neighbors
.append(verts
[2])
2166 verts_V
= self
.get_ordered_verts(
2167 self
.main_object
, all_selected_edges_idx
,
2168 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2171 for i
in range(0, len(verts_V
)):
2172 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2173 # If the vertex nearest to the first point of the second stroke
2174 # is in the first half of the selected verts
2175 if i
>= len(verts_V
) / 2:
2176 first_vert_V_idx
= vert_neighbors
[1]
2179 first_vert_V_idx
= vert_neighbors
[0]
2182 if selection_type
== "TWO_NOT_CONNECTED":
2183 self
.selection_V2_exists
= True
2184 # If the second selection is not closed
2185 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2186 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2188 self
.selection_V2_is_closed
= False
2189 closing_vert_V2_idx
= None
2190 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2193 self
.selection_V2_is_closed
= True
2194 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2196 # Get the neighbors of the first (unselected) vert of the closed selection U
2198 for verts
in single_unselected_verts_and_neighbors
:
2199 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2200 vert_neighbors
.append(verts
[1])
2201 vert_neighbors
.append(verts
[2])
2204 verts_V2
= self
.get_ordered_verts(
2205 self
.main_object
, all_selected_edges_idx
,
2206 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2209 for i
in range(0, len(verts_V2
)):
2210 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2211 # If the vertex nearest to the first point of the second stroke
2212 # is in the first half of the selected verts
2213 if i
>= len(verts_V2
) / 2:
2214 first_vert_V2_idx
= vert_neighbors
[1]
2217 first_vert_V2_idx
= vert_neighbors
[0]
2220 self
.selection_V2_exists
= False
2223 self
.selection_U_exists
= True
2224 self
.selection_V_exists
= False
2225 # If the first selection is not closed
2226 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2227 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2228 self
.selection_U_is_closed
= False
2229 closing_vert_U_idx
= None
2233 self
.main_object
.matrix_world
@
2234 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2237 self
.main_object
.matrix_world
@
2238 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2240 points_first_stroke_tips
= []
2241 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2242 points_first_stroke_tips
.append(
2243 self
.main_splines
.data
.splines
[0].bezier_points
[
2244 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2247 vec_A
= points_tips
[0] - points_tips
[1]
2248 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2250 # Compare the direction of the selection and the first
2251 # grease pencil stroke to determine which is the "first" vertex of the selection
2252 if vec_A
.dot(vec_B
) < 0:
2253 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2255 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2258 self
.selection_U_is_closed
= True
2259 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2261 # Get the neighbors of the first (unselected) vert of the closed selection U
2263 for verts
in single_unselected_verts_and_neighbors
:
2264 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2265 vert_neighbors
.append(verts
[1])
2266 vert_neighbors
.append(verts
[2])
2269 points_first_and_neighbor
= []
2270 points_first_and_neighbor
.append(
2271 self
.main_object
.matrix_world
@
2272 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2274 points_first_and_neighbor
.append(
2275 self
.main_object
.matrix_world
@
2276 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2278 points_first_stroke_tips
= []
2279 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2280 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2282 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2283 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2285 # Compare the direction of the selection and the first grease pencil stroke to
2286 # determine which is the vertex neighbor to the first vertex (unselected) of
2287 # the closed selection. This will determine the direction of the closed selection
2288 if vec_A
.dot(vec_B
) < 0:
2289 first_vert_U_idx
= vert_neighbors
[1]
2291 first_vert_U_idx
= vert_neighbors
[0]
2293 if selection_type
== "TWO_NOT_CONNECTED":
2294 self
.selection_U2_exists
= True
2295 # If the second selection is not closed
2296 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2297 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2299 self
.selection_U2_is_closed
= False
2300 closing_vert_U2_idx
= None
2301 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2303 self
.selection_U2_is_closed
= True
2304 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2306 # Get the neighbors of the first (unselected) vert of the closed selection U
2308 for verts
in single_unselected_verts_and_neighbors
:
2309 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2310 vert_neighbors
.append(verts
[1])
2311 vert_neighbors
.append(verts
[2])
2314 points_first_and_neighbor
= []
2315 points_first_and_neighbor
.append(
2316 self
.main_object
.matrix_world
@
2317 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2319 points_first_and_neighbor
.append(
2320 self
.main_object
.matrix_world
@
2321 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2323 points_last_stroke_tips
= []
2324 points_last_stroke_tips
.append(
2325 self
.main_splines
.data
.splines
[
2326 len(self
.main_splines
.data
.splines
) - 1
2327 ].bezier_points
[0].co
2329 points_last_stroke_tips
.append(
2330 self
.main_splines
.data
.splines
[
2331 len(self
.main_splines
.data
.splines
) - 1
2332 ].bezier_points
[1].co
2334 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2335 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2337 # Compare the direction of the selection and the last grease pencil stroke to
2338 # determine which is the vertex neighbor to the first vertex (unselected) of
2339 # the closed selection. This will determine the direction of the closed selection
2340 if vec_A
.dot(vec_B
) < 0:
2341 first_vert_U2_idx
= vert_neighbors
[1]
2343 first_vert_U2_idx
= vert_neighbors
[0]
2345 self
.selection_U2_exists
= False
2347 elif selection_type
== "NO_SELECTION":
2348 self
.selection_U_exists
= False
2349 self
.selection_V_exists
= False
2351 # Get an ordered list of the vertices of Selection-U
2352 verts_ordered_U
= []
2353 if self
.selection_U_exists
:
2354 verts_ordered_U
= self
.get_ordered_verts(
2355 self
.main_object
, all_selected_edges_idx
,
2356 all_verts_idx
, first_vert_U_idx
,
2357 middle_vertex_idx
, closing_vert_U_idx
2360 # Get an ordered list of the vertices of Selection-U2
2361 verts_ordered_U2
= []
2362 if self
.selection_U2_exists
:
2363 verts_ordered_U2
= self
.get_ordered_verts(
2364 self
.main_object
, all_selected_edges_idx
,
2365 all_verts_idx
, first_vert_U2_idx
,
2366 middle_vertex_idx
, closing_vert_U2_idx
2369 # Get an ordered list of the vertices of Selection-V
2370 verts_ordered_V
= []
2371 if self
.selection_V_exists
:
2372 verts_ordered_V
= self
.get_ordered_verts(
2373 self
.main_object
, all_selected_edges_idx
,
2374 all_verts_idx
, first_vert_V_idx
,
2375 middle_vertex_idx
, closing_vert_V_idx
2377 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2379 # Get an ordered list of the vertices of Selection-V2
2380 verts_ordered_V2
= []
2381 if self
.selection_V2_exists
:
2382 verts_ordered_V2
= self
.get_ordered_verts(
2383 self
.main_object
, all_selected_edges_idx
,
2384 all_verts_idx
, first_vert_V2_idx
,
2385 middle_vertex_idx
, closing_vert_V2_idx
2388 # Check if when there are two-not-connected selections both have the same
2389 # number of verts. If not terminate the script
2390 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2391 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2393 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2395 self
.stopping_errors
= True
2399 # Calculate edges U proportions
2400 # Sum selected edges U lengths
2401 edges_lengths_U
= []
2402 edges_lengths_sum_U
= 0
2404 if self
.selection_U_exists
:
2405 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2409 if self
.selection_U2_exists
:
2410 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2414 # Sum selected edges V lengths
2415 edges_lengths_V
= []
2416 edges_lengths_sum_V
= 0
2418 if self
.selection_V_exists
:
2419 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2423 if self
.selection_V2_exists
:
2424 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2429 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2430 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2431 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2432 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2435 edges_proportions_U
= []
2436 edges_proportions_U
= self
.get_edges_proportions(
2437 edges_lengths_U
, edges_lengths_sum_U
,
2438 self
.selection_U_exists
, self
.edges_U
2440 verts_count_U
= len(edges_proportions_U
) + 1
2442 if self
.selection_U2_exists
:
2443 edges_proportions_U2
= []
2444 edges_proportions_U2
= self
.get_edges_proportions(
2445 edges_lengths_U2
, edges_lengths_sum_U2
,
2446 self
.selection_U2_exists
, self
.edges_V
2450 edges_proportions_V
= []
2451 edges_proportions_V
= self
.get_edges_proportions(
2452 edges_lengths_V
, edges_lengths_sum_V
,
2453 self
.selection_V_exists
, self
.edges_V
2456 if self
.selection_V2_exists
:
2457 edges_proportions_V2
= []
2458 edges_proportions_V2
= self
.get_edges_proportions(
2459 edges_lengths_V2
, edges_lengths_sum_V2
,
2460 self
.selection_V2_exists
, self
.edges_V
2463 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2464 # the actual sketched curves with a "closing segment"
2465 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2466 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2467 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2469 simplified_spline_coords
= []
2470 simplified_curve
= []
2471 ob_simplified_curve
= []
2472 splines_first_v_co
= []
2473 for i
in range(len(self
.main_splines
.data
.splines
)):
2474 # Create a curve object for the actual spline "cyclic extension"
2475 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2476 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2477 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2479 simplified_curve
[i
].dimensions
= "3D"
2482 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2483 spline_coords
.append(bp
.co
)
2486 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2488 # Get the coordinates of the first vert of the actual spline
2489 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2491 # Generate the spline
2492 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2493 # less one because one point is added when the spline is created
2494 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2495 for p
in range(0, len(simplified_spline_coords
[i
])):
2496 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2498 spline
.use_cyclic_u
= True
2500 spline_bp_count
= len(spline
.bezier_points
)
2502 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2503 ob_simplified_curve
[i
].select_set(True)
2504 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2506 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2507 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2508 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2509 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2510 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2512 # Select the "closing segment", and subdivide it
2513 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2514 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2515 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2517 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2518 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2519 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2521 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2523 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2524 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2525 self
.average_gp_segment_length
2528 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=int(segments
))
2530 # Delete the other vertices and make it non-cyclic to
2531 # keep only the needed verts of the "closing segment"
2532 bpy
.ops
.curve
.select_all(action
='INVERT')
2533 bpy
.ops
.curve
.delete(type='VERT')
2534 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2535 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2537 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2538 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2539 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2540 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2542 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2543 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2544 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2546 # Delete the temporal curve
2547 with bpy
.context
.temp_override(selected_objects
=[ob_simplified_curve
[i
]]):
2548 bpy
.ops
.object.delete()
2550 # Get the coords of the points distributed along the sketched strokes,
2551 # with proportions-U of the first selection
2552 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2553 self
.main_splines
.data
.splines
,
2556 sketched_splines_parsed
= []
2558 if self
.selection_U2_exists
:
2559 # Initialize the multidimensional list with the proportions of all the segments
2560 proportions_loops_crossing_strokes
= []
2561 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2562 proportions_loops_crossing_strokes
.append([])
2564 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2565 proportions_loops_crossing_strokes
[i
].append(None)
2567 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2568 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2569 loop_segments_lengths
= []
2571 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2572 # When on the first stroke, add the segment from the selection to the first stroke
2574 loop_segments_lengths
.append(
2575 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2576 pts_on_strokes_with_proportions_U
[0][lp
]).length
2578 # For all strokes except for the last, calculate the distance
2579 # from the actual stroke to the next
2580 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2581 loop_segments_lengths
.append(
2582 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2583 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2585 # When on the last stroke, add the segments
2586 # from the last stroke to the second selection
2587 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2588 loop_segments_lengths
.append(
2589 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2590 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2592 # Calculate full loop length
2593 loop_seg_lengths_sum
= 0
2594 for i
in range(len(loop_segments_lengths
)):
2595 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2597 # Fill the multidimensional list with the proportions of all the segments
2598 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2599 proportions_loops_crossing_strokes
[st
][lp
] = \
2600 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2602 # Calculate proportions for each stroke
2603 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2604 actual_stroke_spline
= []
2605 # Needs to be a list for the "distribute_pts" method
2606 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2608 # Calculate the proportions for the actual stroke.
2609 actual_edges_proportions_U
= []
2610 for i
in range(len(edges_proportions_U
)):
2613 # Sum the proportions of this loop up to the actual.
2614 for t
in range(0, st
+ 1):
2615 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2616 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2617 # and the proportions refer to edges, so we start at the element 1
2618 # of proportions_loops_crossing_strokes instead of element 0
2619 actual_edges_proportions_U
.append(
2620 edges_proportions_U
[i
] -
2621 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2623 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2624 sketched_splines_parsed
.append(points_actual_spline
[0])
2626 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2628 # If the selection type is "TWO_NOT_CONNECTED" replace the
2629 # points of the last spline with the points in the "target" selection
2630 if selection_type
== "TWO_NOT_CONNECTED":
2631 if self
.selection_U2_exists
:
2632 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2633 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2634 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2636 # Create temporary curves along the "control-points" found
2637 # on the sketched curves and the mesh selection
2638 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2639 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2640 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2641 ob_ctrl_pts
.data
= me
2642 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2649 for i
in range(0, verts_count_U
):
2650 vert_num_in_spline
= 1
2652 if self
.selection_U_exists
:
2653 ob_ctrl_pts
.data
.vertices
.add(1)
2654 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2655 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2657 vert_num_in_spline
+= 1
2659 for t
in range(0, len(sketched_splines_parsed
)):
2660 ob_ctrl_pts
.data
.vertices
.add(1)
2661 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2662 v
.co
= sketched_splines_parsed
[t
][i
]
2664 if vert_num_in_spline
> 1:
2665 ob_ctrl_pts
.data
.edges
.add(1)
2666 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2667 len(ob_ctrl_pts
.data
.vertices
) - 2
2668 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2669 len(ob_ctrl_pts
.data
.vertices
) - 1
2672 first_verts
.append(v
.index
)
2675 second_verts
.append(v
.index
)
2677 if t
== len(sketched_splines_parsed
) - 1:
2678 last_verts
.append(v
.index
)
2681 vert_num_in_spline
+= 1
2683 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2684 ob_ctrl_pts
.select_set(True)
2685 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2687 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2688 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2689 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2691 # Determine which loops-U will be "Cyclic"
2692 for i
in range(0, len(first_verts
)):
2693 # When there is Cyclic Cross there is no need of
2694 # Automatic Join, (and there are at least three strokes)
2695 if self
.automatic_join
and not self
.cyclic_cross
and \
2696 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2698 v
= ob_ctrl_pts
.data
.vertices
2699 first_point_co
= v
[first_verts
[i
]].co
2700 second_point_co
= v
[second_verts
[i
]].co
2701 last_point_co
= v
[last_verts
[i
]].co
2703 # Coordinates of the point in the center of both the first and last verts.
2705 (first_point_co
[0] + last_point_co
[0]) / 2,
2706 (first_point_co
[1] + last_point_co
[1]) / 2,
2707 (first_point_co
[2] + last_point_co
[2]) / 2
2709 vec_A
= second_point_co
- first_point_co
2710 vec_B
= second_point_co
- Vector(verts_center_co
)
2712 # Calculate the length of the first segment of the loop,
2713 # and the length it would have after moving the first vert
2714 # to the middle position between first and last
2715 length_original
= (second_point_co
- first_point_co
).length
2716 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2718 angle
= vec_A
.angle(vec_B
) / pi
2720 # If the target length doesn't stretch too much, and the
2721 # its angle doesn't change to much either
2722 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2723 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2725 cyclic_loops_U
.append(True)
2726 # Move the first vert to the center coordinates
2727 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2728 # Select the last verts from Cyclic loops, for later deletion all at once
2729 v
[last_verts
[i
]].select
= True
2731 cyclic_loops_U
.append(False)
2733 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2734 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2735 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2736 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2738 cyclic_loops_U
.append(True)
2740 cyclic_loops_U
.append(False)
2742 # The cyclic_loops_U list needs to be reversed.
2743 cyclic_loops_U
.reverse()
2745 # Delete the previously selected (last_)verts.
2746 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2747 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2748 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2750 # Create curves from control points.
2751 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2752 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2753 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2754 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2755 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2757 # Make Cyclic the splines designated as Cyclic.
2758 for i
in range(0, len(cyclic_loops_U
)):
2759 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2761 # Get the coords of all points on first loop-U, for later comparison with its
2762 # subdivided version, to know which points of the loops-U are crossed by the
2763 # original strokes. The indices will be the same for the other loops-U
2764 if self
.loops_on_strokes
:
2765 coords_loops_U_control_points
= []
2766 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2767 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2769 tuple(coords_loops_U_control_points
)
2771 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2772 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2773 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2775 edges_V_count
= len(edges_proportions_V
)
2777 # The Follow precision will vary depending on the number of Follow face-loops
2778 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2779 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2781 # Subdivide the curves
2782 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2784 # The verts position shifting that happens with splines subdivision.
2785 # For later reorder splines points
2786 verts_position_shift
= curve_cuts
+ 1
2787 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2789 # Reorder coordinates of the points of each spline to put the first point of
2790 # the spline starting at the position it was the first point before sudividing
2791 # the curve. And make a new curve object per spline (to handle memory better later)
2792 splines_U_objects
= []
2793 for i
in range(len(ob_curves_surf
.data
.splines
)):
2794 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2795 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2796 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2798 spline_U_curve
.dimensions
= "3D"
2800 # Add points to the spline in the new curve object
2801 ob_spline_U
.data
.splines
.new('BEZIER')
2802 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2803 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2804 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2805 point_index
= t
+ verts_position_shift
2807 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2810 # to avoid adding the first point since it's added when the spline is created
2812 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2813 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2814 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2816 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2817 # Add a last point at the same location as the first one
2818 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2819 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2820 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2822 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2824 splines_U_objects
.append(ob_spline_U
)
2825 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2826 ob_spline_U
.select_set(True)
2827 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2829 # When option "Loops on strokes" is active each "Cross" loop will have
2830 # its own proportions according to where the original strokes "touch" them
2831 if self
.loops_on_strokes
:
2832 # Get the indices of points where the original strokes "touch" loops-U
2833 points_U_crossed_by_strokes
= []
2834 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2835 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2836 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2837 points_U_crossed_by_strokes
.append(i
)
2839 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2840 edge_order_number_for_splines
= {}
2841 if self
.selection_V_exists
:
2842 # For two-connected selections add a first hypothetic stroke at the beginning.
2843 if selection_type
== "TWO_CONNECTED":
2844 edge_order_number_for_splines
[0] = 0
2846 for i
in range(len(self
.main_splines
.data
.splines
)):
2847 sp
= self
.main_splines
.data
.splines
[i
]
2848 v_idx
, _dist_temp
= self
.shortest_distance(
2850 sp
.bezier_points
[0].co
,
2851 verts_ordered_V_indices
2853 # Get the position (edges count) of the vert v_idx in the selected chain V
2854 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2856 # For two-connected selections the strokes go after the
2857 # hypothetic stroke added before, so the index adds one per spline
2858 if selection_type
== "TWO_CONNECTED":
2859 spline_number
= i
+ 1
2863 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2865 # Get the first and last verts indices for later comparison
2868 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2871 if self
.selection_V_is_closed
:
2872 # If there is no last stroke on the last vertex (same as first vertex),
2873 # add a hypothetic spline at last vert order
2874 if first_v_idx
!= last_v_idx
:
2875 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2876 len(verts_ordered_V_indices
) - 1
2878 if self
.cyclic_cross
:
2879 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2880 len(verts_ordered_V_indices
) - 2
2881 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2882 len(verts_ordered_V_indices
) - 1
2884 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2885 len(verts_ordered_V_indices
) - 1
2887 # Get the coords of the points distributed along the
2888 # "crossing curves", with appropriate proportions-V
2889 surface_splines_parsed
= []
2890 for i
in range(len(splines_U_objects
)):
2891 sp_ob
= splines_U_objects
[i
]
2892 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2893 if self
.loops_on_strokes
:
2894 # Segments distances from stroke to stroke
2897 segments_distances
= []
2898 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2899 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2905 dist
+= (last_p
- actual_p
).length
2907 if t
in points_U_crossed_by_strokes
:
2908 segments_distances
.append(dist
)
2915 # Calculate Proportions.
2916 used_edges_proportions_V
= []
2917 for t
in range(len(segments_distances
)):
2918 if self
.selection_V_exists
:
2920 order_number_last_stroke
= 0
2922 segment_edges_length_V
= 0
2923 segment_edges_length_V2
= 0
2924 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2925 segment_edges_length_V
+= edges_lengths_V
[order
]
2926 if self
.selection_V2_exists
:
2927 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2929 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2930 # Calculate each "sub-segment" (the ones between each stroke) length
2931 if self
.selection_V2_exists
:
2932 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2933 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2934 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2935 (segment_edges_length_V2
- segment_edges_length_V
) /
2936 len(splines_U_objects
) * i
)
2938 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2940 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2941 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2943 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2945 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2948 for _c
in range(self
.edges_V
):
2949 # Calculate each "sub-segment" (the ones between each stroke) length
2950 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2951 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2953 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2954 surface_splines_parsed
.append(actual_spline
[0])
2957 if self
.selection_V2_exists
:
2958 used_edges_proportions_V
= []
2959 for p
in range(len(edges_proportions_V
)):
2960 used_edges_proportions_V
.append(
2961 edges_proportions_V2
[p
] -
2962 ((edges_proportions_V2
[p
] -
2963 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2966 used_edges_proportions_V
= edges_proportions_V
2968 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2969 surface_splines_parsed
.append(actual_spline
[0])
2971 # Set the verts of the first and last splines to the locations
2972 # of the respective verts in the selections
2973 if self
.selection_V_exists
:
2974 for i
in range(0, len(surface_splines_parsed
[0])):
2975 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2976 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2978 if selection_type
== "TWO_NOT_CONNECTED":
2979 if self
.selection_V2_exists
:
2980 for i
in range(0, len(surface_splines_parsed
[0])):
2981 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2983 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2984 # merge the verts of the tips of the loops when they are "near enough"
2985 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2986 # Join the tips of "Follow" loops that are near enough and must be "closed"
2987 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2988 for i
in range(len(surface_splines_parsed
[0])):
2989 sp
= surface_splines_parsed
2990 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2992 verts_middle_position_co
= [
2993 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2994 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2995 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2997 points_original
= []
2998 points_original
.append(sp
[1][i
])
2999 points_original
.append(sp
[0][i
])
3002 points_target
.append(sp
[1][i
])
3003 points_target
.append(Vector(verts_middle_position_co
))
3005 vec_A
= points_original
[0] - points_original
[1]
3006 vec_B
= points_target
[0] - points_target
[1]
3007 # check for zero angles, not sure if it is a great fix
3008 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3009 angle
= vec_A
.angle(vec_B
) / pi
3010 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3015 # If after moving the verts to the middle point, the segment doesn't stretch too much
3016 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3017 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3019 # Avoid joining when the actual loop must be merged with the original mesh
3020 if not (self
.selection_U_exists
and i
== 0) and \
3021 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3023 # Change the coords of both verts to the middle position
3024 surface_splines_parsed
[0][i
] = verts_middle_position_co
3025 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3027 # Delete object with control points and object from grease pencil conversion
3028 with bpy
.context
.temp_override(selected_objects
=[ob_ctrl_pts
]):
3029 bpy
.ops
.object.delete()
3031 with bpy
.context
.temp_override(selected_objects
=splines_U_objects
):
3032 bpy
.ops
.object.delete()
3036 # Get all verts coords
3037 all_surface_verts_co
= []
3038 for i
in range(0, len(surface_splines_parsed
)):
3039 # Get coords of all verts and make a list with them
3040 for pt_co
in surface_splines_parsed
[i
]:
3041 all_surface_verts_co
.append(pt_co
)
3043 # Define verts for each face
3044 all_surface_faces
= []
3045 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3046 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3047 all_surface_faces
.append(
3048 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3049 i
+ len(surface_splines_parsed
[0]) + 1]
3052 surf_me_name
= "SURFSKIO_surface"
3053 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3054 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3055 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3056 ob_surface
.location
= (0.0, 0.0, 0.0)
3057 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3058 ob_surface
.scale
= (1.0, 1.0, 1.0)
3060 # Select all the "unselected but participating" verts, from closed selection
3061 # or double selections with middle-vertex, for later join with remove doubles
3062 for v_idx
in single_unselected_verts
:
3063 self
.main_object
.data
.vertices
[v_idx
].select
= True
3065 # Join the new mesh to the main object
3066 ob_surface
.select_set(True)
3067 self
.main_object
.select_set(True)
3068 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3070 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3072 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3074 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3075 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3076 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3084 global global_shade_smooth
3085 if global_shade_smooth
:
3086 bpy
.ops
.object.shade_smooth()
3088 bpy
.ops
.object.shade_flat()
3089 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3095 def execute(self
, context
):
3097 if bpy
.ops
.object.mode_set
.poll():
3098 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3101 global global_mesh_object
3102 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3103 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3104 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3105 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3106 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3108 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3110 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3114 if not self
.is_fill_faces
:
3115 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3116 value
='True, False, False')
3118 # Build splines from the "last saved splines".
3119 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3120 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3121 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3123 last_saved_curve
.dimensions
= "3D"
3125 for sp
in self
.last_strokes_splines_coords
:
3126 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3127 # less one because one point is added when the spline is created
3128 spline
.bezier_points
.add(len(sp
) - 1)
3129 for p
in range(0, len(sp
)):
3130 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3132 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3134 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3135 self
.main_splines
.select_set(True)
3136 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3138 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3140 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3141 # Important to make it vector first and then automatic, otherwise the
3142 # tips handles get too big and distort the shrinkwrap results later
3143 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3144 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3145 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3146 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3148 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3150 if self
.is_crosshatch
:
3151 strokes_for_crosshatch
= True
3152 strokes_for_rectangular_surface
= False
3154 strokes_for_rectangular_surface
= True
3155 strokes_for_crosshatch
= False
3157 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3159 if strokes_for_rectangular_surface
:
3160 self
.rectangular_surface(context
)
3161 elif strokes_for_crosshatch
:
3162 self
.crosshatch_surface_execute(context
)
3164 #Set Shade smooth to new polygons
3165 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3166 global global_shade_smooth
3167 if global_shade_smooth
:
3168 bpy
.ops
.object.shade_smooth()
3170 bpy
.ops
.object.shade_flat()
3172 # Delete main splines
3173 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3174 if self
.keep_strokes
:
3175 self
.main_splines
.name
= "keep_strokes"
3176 self
.main_splines
.data
.bevel_depth
= 0.001
3177 if "keep_strokes_material" in bpy
.data
.materials
:
3178 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3180 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3181 mat
.diffuse_color
= (1, 0, 0, 0)
3182 mat
.specular_color
= (1, 0, 0)
3183 mat
.specular_intensity
= 0.0
3185 self
.main_splines
.data
.materials
.append(mat
)
3187 with bpy
.context
.temp_override(selected_objects
=[self
.main_splines
]):
3188 bpy
.ops
.object.delete()
3190 # Delete grease pencil strokes
3191 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3193 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3197 # Delete annotations
3198 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3200 bpy
.context
.annotation_data
.layers
.active
.clear()
3204 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3205 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3206 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3207 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3208 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3209 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3210 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3211 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3213 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3214 self
.main_object
.select_set(True)
3215 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3217 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3223 def invoke(self
, context
, event
):
3225 if bpy
.ops
.object.mode_set
.poll():
3226 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3228 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3229 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3230 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3231 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3232 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3233 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3236 global global_mesh_object
3237 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3238 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3239 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3240 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3242 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3247 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3249 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3250 value
='True, False, False')
3252 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3253 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3255 self
.is_fill_faces
= False
3256 self
.stopping_errors
= False
3257 self
.last_strokes_splines_coords
= []
3259 # Determine the type of the strokes
3260 self
.strokes_type
= get_strokes_type(context
)
3262 # Check if it will be used grease pencil strokes or curves
3263 # If there are strokes to be used
3264 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3265 if self
.strokes_type
== "GP_STROKES":
3266 # Convert grease pencil strokes to curve
3267 global global_gpencil_object
3268 gp
= bpy
.data
.objects
[global_gpencil_object
]
3269 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3270 self
.using_external_curves
= False
3272 elif self
.strokes_type
== "GP_ANNOTATION":
3273 # Convert grease pencil strokes to curve
3274 gp
= bpy
.context
.annotation_data
3275 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3276 self
.using_external_curves
= False
3278 elif self
.strokes_type
== "EXTERNAL_CURVE":
3279 global global_curve_object
3280 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3281 self
.using_external_curves
= True
3283 # Make sure there are no objects left from erroneous
3284 # executions of this operator, with the reserved names used here
3285 for o
in bpy
.data
.objects
:
3286 if o
.name
.find("SURFSKIO_") != -1:
3287 with bpy
.context
.temp_override(selected_objects
=[o
]):
3288 bpy
.ops
.object.delete()
3290 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3292 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3294 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3296 # Deselect all points of the curve
3297 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3298 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3299 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3301 # Delete splines with only a single isolated point
3302 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3303 sp
= self
.temporary_curve
.data
.splines
[i
]
3305 if len(sp
.bezier_points
) == 1:
3306 sp
.bezier_points
[0].select_control_point
= True
3308 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3309 bpy
.ops
.curve
.delete(type='VERT')
3310 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3312 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3313 self
.temporary_curve
.select_set(True)
3314 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3316 # Set a minimum number of points for crosshatch
3317 minimum_points_num
= 15
3319 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3320 # Check if the number of points of each curve has at least the number of points
3321 # of minimum_points_num, which is a bit more than the face-loops limit.
3322 # If not, subdivide to reach at least that number of points
3323 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3324 sp
= self
.temporary_curve
.data
.splines
[i
]
3326 if len(sp
.bezier_points
) < minimum_points_num
:
3327 for bp
in sp
.bezier_points
:
3328 bp
.select_control_point
= True
3330 if (len(sp
.bezier_points
) - 1) != 0:
3331 # Formula to get the number of cuts that will make a curve
3332 # of N number of points have near to "minimum_points_num"
3333 # points, when subdividing with this number of cuts
3334 subdivide_cuts
= int(
3335 (minimum_points_num
- len(sp
.bezier_points
)) /
3336 (len(sp
.bezier_points
) - 1)
3341 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3342 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3344 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3346 # Detect if the strokes are a crosshatch and do it if it is
3347 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3349 if not self
.is_crosshatch
:
3350 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3351 self
.temporary_curve
.select_set(True)
3352 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3354 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3356 # Set a minimum number of points for rectangular surfaces
3357 minimum_points_num
= 60
3359 # Check if the number of points of each curve has at least the number of points
3360 # of minimum_points_num, which is a bit more than the face-loops limit.
3361 # If not, subdivide to reach at least that number of points
3362 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3363 sp
= self
.temporary_curve
.data
.splines
[i
]
3365 if len(sp
.bezier_points
) < minimum_points_num
:
3366 for bp
in sp
.bezier_points
:
3367 bp
.select_control_point
= True
3369 if (len(sp
.bezier_points
) - 1) != 0:
3370 # Formula to get the number of cuts that will make a curve of
3371 # N number of points have near to "minimum_points_num" points,
3372 # when subdividing with this number of cuts
3373 subdivide_cuts
= int(
3374 (minimum_points_num
- len(sp
.bezier_points
)) /
3375 (len(sp
.bezier_points
) - 1)
3380 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3381 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3383 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3385 # Save coordinates of the actual strokes (as the "last saved splines")
3386 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3387 self
.last_strokes_splines_coords
.append([])
3388 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3389 coords
= self
.temporary_curve
.matrix_world
@ \
3390 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3391 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3393 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3394 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3395 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3396 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3397 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3398 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3401 (first_p_co
[0] + last_p_co
[0]) / 2,
3402 (first_p_co
[1] + last_p_co
[1]) / 2,
3403 (first_p_co
[2] + last_p_co
[2]) / 2
3406 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3407 self
.last_strokes_splines_coords
[sp_idx
][
3408 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3410 tuple(self
.last_strokes_splines_coords
)
3412 # Estimation of the average length of the segments between
3413 # each point of the grease pencil strokes.
3414 # Will be useful to determine whether a curve should be made "Cyclic"
3415 segments_lengths_sum
= 0
3417 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3418 for i
in range(0, len(random_spline
)):
3419 if i
!= 0 and len(random_spline
) - 1 >= i
:
3420 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3423 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3425 # Delete temporary strokes curve object
3426 with bpy
.context
.temp_override(selected_objects
=[self
.temporary_curve
]):
3427 bpy
.ops
.object.delete()
3429 # Set again since "execute()" will turn it again to its initial value
3430 self
.execute(context
)
3432 if not self
.stopping_errors
:
3433 # Delete grease pencil strokes
3434 if self
.strokes_type
== "GP_STROKES":
3436 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3440 # Delete annotation strokes
3441 elif self
.strokes_type
== "GP_ANNOTATION":
3443 bpy
.context
.annotation_data
.layers
.active
.clear()
3447 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3448 with bpy
.context
.temp_override(selected_objects
=[self
.original_curve
]):
3449 bpy
.ops
.object.delete()
3450 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3456 elif self
.strokes_type
== "SELECTION_ALONE":
3457 self
.is_fill_faces
= True
3458 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3460 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3462 if created_faces_count
== 0:
3463 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3464 return {"CANCELLED"}
3468 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3469 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3472 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3473 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3476 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3477 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3479 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3482 elif self
.strokes_type
== "NO_STROKES":
3483 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3486 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3487 self
.report({'WARNING'}, "All splines must be Bezier.")
3493 # ----------------------------
3495 class MESH_OT_SURFSK_init(Operator
):
3496 bl_idname
= "mesh.surfsk_init"
3497 bl_label
= "Bsurfaces initialize"
3498 bl_description
= "Add an empty mesh object with useful settings"
3499 bl_options
= {'REGISTER', 'UNDO'}
3501 def execute(self
, context
):
3503 bs
= bpy
.context
.scene
.bsurfaces
3505 if bpy
.ops
.object.mode_set
.poll():
3506 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3508 global global_shade_smooth
3509 global global_mesh_object
3510 global global_gpencil_object
3512 if bs
.SURFSK_mesh
== None:
3513 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3514 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3515 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3516 mesh_object
.select_set(True)
3517 bpy
.context
.view_layer
.objects
.active
= mesh_object
3519 mesh_object
.show_all_edges
= True
3520 mesh_object
.display_type
= 'SOLID'
3521 mesh_object
.show_wire
= True
3523 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3524 if global_shade_smooth
:
3525 bpy
.ops
.object.shade_smooth()
3527 bpy
.ops
.object.shade_flat()
3529 color_red
= [1.0, 0.0, 0.0, 0.3]
3530 material
= makeMaterial("BSurfaceMesh", color_red
)
3531 mesh_object
.data
.materials
.append(material
)
3532 modifier
= mesh_object
.modifiers
.new("", 'SHRINKWRAP')
3533 if self
.active_object
is not None:
3534 modifier
.target
= self
.active_object
3535 modifier
.wrap_method
= 'TARGET_PROJECT'
3536 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3537 modifier
.show_on_cage
= True
3539 global_mesh_object
= mesh_object
.name
3540 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3542 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3543 bpy
.context
.scene
.tool_settings
.use_snap
= True
3544 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3545 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3546 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3547 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3548 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3550 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3551 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3553 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3554 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3555 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')
3556 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3557 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3558 gpencil_object
.select_set(True)
3559 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3560 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3561 global_gpencil_object
= gpencil_object
.name
3562 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3563 gpencil_object
.data
.stroke_depth_order
= '3D'
3564 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3565 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3567 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3568 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3569 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3571 def invoke(self
, context
, event
):
3572 if bpy
.context
.active_object
:
3573 self
.active_object
= bpy
.context
.active_object
3575 self
.active_object
= None
3577 self
.execute(context
)
3581 # ----------------------------
3582 # Add modifiers operator
3583 class MESH_OT_SURFSK_add_modifiers(Operator
):
3584 bl_idname
= "mesh.surfsk_add_modifiers"
3585 bl_label
= "Add Mirror and others modifiers"
3586 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3587 bl_options
= {'REGISTER', 'UNDO'}
3589 def execute(self
, context
):
3591 bs
= bpy
.context
.scene
.bsurfaces
3593 if bpy
.ops
.object.mode_set
.poll():
3594 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3596 if bs
.SURFSK_mesh
== None:
3597 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3599 mesh_object
= bs
.SURFSK_mesh
3602 mesh_object
.select_set(True)
3604 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3607 bpy
.context
.view_layer
.objects
.active
= mesh_object
3610 shrinkwrap
= next(mod
for mod
in mesh_object
.modifiers
3611 if mod
.type == 'SHRINKWRAP')
3613 shrinkwrap
= mesh_object
.modifiers
.new("", 'SHRINKWRAP')
3614 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3615 shrinkwrap
.target
= self
.active_object
3616 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3617 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3618 shrinkwrap
.show_on_cage
= True
3619 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3622 mirror
= next(mod
for mod
in mesh_object
.modifiers
3623 if mod
.type == 'MIRROR')
3625 mirror
= mesh_object
.modifiers
.new("", 'MIRROR')
3626 mirror
.use_clip
= True
3629 _subsurf
= next(mod
for mod
in mesh_object
.modifiers
3630 if mod
.type == 'SUBSURF')
3632 _subsurf
= mesh_object
.modifiers
.new("", 'SUBSURF')
3635 solidify
= next(mod
for mod
in mesh_object
.modifiers
3636 if mod
.type == 'SOLIDIFY')
3638 solidify
= mesh_object
.modifiers
.new("", 'SOLIDIFY')
3639 solidify
.thickness
= 0.01
3643 def invoke(self
, context
, event
):
3644 if bpy
.context
.active_object
:
3645 self
.active_object
= bpy
.context
.active_object
3647 self
.active_object
= None
3649 self
.execute(context
)
3653 # ----------------------------
3654 # Edit surface operator
3655 class MESH_OT_SURFSK_edit_surface(Operator
):
3656 bl_idname
= "mesh.surfsk_edit_surface"
3657 bl_label
= "Bsurfaces edit surface"
3658 bl_description
= "Edit surface mesh"
3659 bl_options
= {'REGISTER', 'UNDO'}
3661 def execute(self
, context
):
3662 if bpy
.ops
.object.mode_set
.poll():
3663 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3664 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3665 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3666 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3667 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3668 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3670 def invoke(self
, context
, event
):
3672 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3673 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3674 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3675 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3677 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3680 self
.execute(context
)
3684 # ----------------------------
3685 # Add strokes operator
3686 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3687 bl_idname
= "gpencil.surfsk_add_strokes"
3688 bl_label
= "Bsurfaces add strokes"
3689 bl_description
= "Add the grease pencil strokes"
3690 bl_options
= {'REGISTER', 'UNDO'}
3692 def execute(self
, context
):
3693 if bpy
.ops
.object.mode_set
.poll():
3694 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3695 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3697 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3698 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3699 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3700 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3704 def invoke(self
, context
, event
):
3706 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3708 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3711 self
.execute(context
)
3715 # ----------------------------
3716 # Edit strokes operator
3717 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3718 bl_idname
= "gpencil.surfsk_edit_strokes"
3719 bl_label
= "Bsurfaces edit strokes"
3720 bl_description
= "Edit the grease pencil strokes"
3721 bl_options
= {'REGISTER', 'UNDO'}
3723 def execute(self
, context
):
3724 if bpy
.ops
.object.mode_set
.poll():
3725 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3726 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3728 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3730 gpencil_object
.select_set(True)
3731 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3733 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3735 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3739 def invoke(self
, context
, event
):
3741 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3743 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3746 self
.execute(context
)
3750 # ----------------------------
3751 # Convert annotation to curves operator
3752 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3753 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3754 bl_label
= "Convert annotation to curves"
3755 bl_description
= "Convert annotation to curves for editing"
3756 bl_options
= {'REGISTER', 'UNDO'}
3758 def execute(self
, context
):
3760 if bpy
.ops
.object.mode_set
.poll():
3761 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3763 # Convert annotation to curve
3764 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3767 # Delete annotation strokes
3769 bpy
.context
.annotation_data
.layers
.active
.clear()
3774 curve
.select_set(True)
3775 bpy
.context
.view_layer
.objects
.active
= curve
3777 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3781 def invoke(self
, context
, event
):
3783 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3785 _strokes_num
= len(strokes
)
3787 self
.report({'WARNING'}, "Not active annotation")
3790 self
.execute(context
)
3794 # ----------------------------
3795 # Convert strokes to curves operator
3796 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3797 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3798 bl_label
= "Convert strokes to curves"
3799 bl_description
= "Convert grease pencil strokes to curves for editing"
3800 bl_options
= {'REGISTER', 'UNDO'}
3802 def execute(self
, context
):
3804 if bpy
.ops
.object.mode_set
.poll():
3805 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3807 # Convert grease pencil strokes to curve
3808 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3809 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3812 # Delete grease pencil strokes
3814 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3820 curve
.select_set(True)
3821 bpy
.context
.view_layer
.objects
.active
= curve
3823 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3827 def invoke(self
, context
, event
):
3829 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3831 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3834 self
.execute(context
)
3838 # ----------------------------
3840 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3841 bl_idname
= "gpencil.surfsk_add_annotation"
3842 bl_label
= "Bsurfaces add annotation"
3843 bl_description
= "Add annotation"
3844 bl_options
= {'REGISTER', 'UNDO'}
3846 def execute(self
, context
):
3847 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3848 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3852 def invoke(self
, context
, event
):
3854 self
.execute(context
)
3859 # ----------------------------
3860 # Edit curve operator
3861 class CURVE_OT_SURFSK_edit_curve(Operator
):
3862 bl_idname
= "curve.surfsk_edit_curve"
3863 bl_label
= "Bsurfaces edit curve"
3864 bl_description
= "Edit curve"
3865 bl_options
= {'REGISTER', 'UNDO'}
3867 def execute(self
, context
):
3868 if bpy
.ops
.object.mode_set
.poll():
3869 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3870 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3871 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3872 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3873 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3875 def invoke(self
, context
, event
):
3877 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3879 self
.report({'WARNING'}, "Specify the name of the object with curve")
3882 self
.execute(context
)
3886 # ----------------------------
3888 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3889 bl_idname
= "curve.surfsk_reorder_splines"
3890 bl_label
= "Bsurfaces reorder splines"
3891 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3892 bl_options
= {'REGISTER', 'UNDO'}
3894 def execute(self
, context
):
3895 objects_to_delete
= []
3896 # Convert grease pencil strokes to curve.
3897 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3898 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3899 for ob
in bpy
.context
.selected_objects
:
3900 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3901 GP_strokes_curve
= ob
3903 # GP_strokes_curve = bpy.context.object
3904 objects_to_delete
.append(GP_strokes_curve
)
3906 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3907 GP_strokes_curve
.select_set(True)
3908 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3910 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3911 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3912 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3913 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3915 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3916 GP_strokes_mesh
= bpy
.context
.object
3917 objects_to_delete
.append(GP_strokes_mesh
)
3919 GP_strokes_mesh
.data
.resolution_u
= 1
3920 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3922 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3923 self
.main_curve
.select_set(True)
3924 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3926 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3927 curves_duplicate_1
= bpy
.context
.object
3928 objects_to_delete
.append(curves_duplicate_1
)
3930 minimum_points_num
= 500
3932 # Some iterations since the subdivision operator
3933 # has a limit of 100 subdivisions per iteration
3934 for x
in range(round(minimum_points_num
/ 100)):
3935 # Check if the number of points of each curve has at least the number of points
3936 # of minimum_points_num. If not, subdivide to reach at least that number of points
3937 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3938 sp
= curves_duplicate_1
.data
.splines
[i
]
3940 if len(sp
.bezier_points
) < minimum_points_num
:
3941 for bp
in sp
.bezier_points
:
3942 bp
.select_control_point
= True
3944 if (len(sp
.bezier_points
) - 1) != 0:
3945 # Formula to get the number of cuts that will make a curve of N
3946 # number of points have near to "minimum_points_num" points,
3947 # when subdividing with this number of cuts
3948 subdivide_cuts
= int(
3949 (minimum_points_num
- len(sp
.bezier_points
)) /
3950 (len(sp
.bezier_points
) - 1)
3955 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3956 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3957 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3958 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3960 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3961 curves_duplicate_2
= bpy
.context
.object
3962 objects_to_delete
.append(curves_duplicate_2
)
3964 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3965 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3966 curves_duplicate_2
.select_set(True)
3967 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3969 shrinkwrap
= curves_duplicate_2
.modifiers
.new("", 'SHRINKWRAP')
3970 shrinkwrap
.wrap_method
= "NEAREST_VERTEX"
3971 shrinkwrap
.target
= GP_strokes_mesh
3972 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap
.name
)
3974 # Get the distance of each vert from its original position to its position with Shrinkwrap
3975 nearest_points_coords
= {}
3976 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3977 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3978 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3979 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3981 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3982 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3985 shortest_dist
= (bp_1_co
- bp_2_co
).length
3986 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3987 "%.4f" % bp_2_co
[1],
3988 "%.4f" % bp_2_co
[2])
3990 dist
= (bp_1_co
- bp_2_co
).length
3992 if dist
< shortest_dist
:
3993 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3994 "%.4f" % bp_2_co
[1],
3995 "%.4f" % bp_2_co
[2])
3996 shortest_dist
= dist
3998 # Get all coords of GP strokes points, for comparison
3999 GP_strokes_coords
= []
4000 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
4001 GP_strokes_coords
.append(
4002 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4003 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4004 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4005 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4008 # Check the point of the GP strokes with the same coords as
4009 # the nearest points of the curves (with shrinkwrap)
4011 # Dictionary with GP stroke index as index, and a list as value.
4012 # The list has as index the point index of the GP stroke
4013 # nearest to the spline, and as value the spline index
4014 GP_connection_points
= {}
4015 for gp_st_idx
in range(len(GP_strokes_coords
)):
4016 GPvert_spline_relationship
= {}
4018 for splines_st_idx
in range(len(nearest_points_coords
)):
4019 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4020 GPvert_spline_relationship
[
4021 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4024 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4026 # Get the splines new order
4027 splines_new_order
= []
4028 for i
in GP_connection_points
:
4029 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4032 splines_new_order
.append(GP_connection_points
[i
][k
])
4035 curve_original_name
= self
.main_curve
.name
4037 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4038 self
.main_curve
.select_set(True)
4039 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4041 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4043 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4044 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4045 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4047 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4048 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4050 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4051 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4052 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4054 # Get the names of the separated splines objects in the original order
4055 splines_unordered
= {}
4056 for o
in bpy
.data
.objects
:
4057 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4058 spline_order_string
= o
.name
.partition(".")[2]
4060 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4061 spline_order_index
= int(spline_order_string
) - 1
4062 splines_unordered
[spline_order_index
] = o
.name
4064 # Join all splines objects in final order
4065 for order_idx
in splines_new_order
:
4066 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4067 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4068 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4069 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4071 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4073 # Go back to the original name of the curves object.
4074 bpy
.context
.object.name
= curve_original_name
4076 # Delete all unused objects
4077 with bpy
.context
.temp_override(selected_objects
=objects_to_delete
):
4078 bpy
.ops
.object.delete()
4080 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4081 bpy
.data
.objects
[curve_original_name
].select_set(True)
4082 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4084 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4085 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4088 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4095 def invoke(self
, context
, event
):
4096 self
.main_curve
= bpy
.context
.object
4097 there_are_GP_strokes
= False
4100 # Get the active grease pencil layer
4101 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4104 there_are_GP_strokes
= True
4108 if there_are_GP_strokes
:
4109 self
.execute(context
)
4110 self
.report({'INFO'}, "Splines have been reordered")
4112 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4116 # ----------------------------
4117 # Set first points operator
4118 class CURVE_OT_SURFSK_first_points(Operator
):
4119 bl_idname
= "curve.surfsk_first_points"
4120 bl_label
= "Bsurfaces set first points"
4121 bl_description
= "Set the selected points as the first point of each spline"
4122 bl_options
= {'REGISTER', 'UNDO'}
4124 def execute(self
, context
):
4125 splines_to_invert
= []
4127 # Check non-cyclic splines to invert
4128 for i
in range(len(self
.main_curve
.data
.splines
)):
4129 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4131 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4132 if b_points
[len(b_points
) - 1].select_control_point
:
4133 splines_to_invert
.append(i
)
4135 # Reorder points of cyclic splines, and set all handles to "Automatic"
4137 # Check first selected point
4138 cyclic_splines_new_first_pt
= {}
4139 for i
in self
.cyclic_splines
:
4140 sp
= self
.main_curve
.data
.splines
[i
]
4142 for t
in range(len(sp
.bezier_points
)):
4143 bp
= sp
.bezier_points
[t
]
4144 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4145 cyclic_splines_new_first_pt
[i
] = t
4146 break # To take only one if there are more
4149 for spline_idx
in cyclic_splines_new_first_pt
:
4150 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4152 spline_old_coords
= []
4153 for bp_old
in sp
.bezier_points
:
4154 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4156 left_handle_type
= str(bp_old
.handle_left_type
)
4157 left_handle_length
= float(bp_old
.handle_left
.length
)
4159 float(bp_old
.handle_left
.x
),
4160 float(bp_old
.handle_left
.y
),
4161 float(bp_old
.handle_left
.z
)
4163 right_handle_type
= str(bp_old
.handle_right_type
)
4164 right_handle_length
= float(bp_old
.handle_right
.length
)
4165 right_handle_xyz
= (
4166 float(bp_old
.handle_right
.x
),
4167 float(bp_old
.handle_right
.y
),
4168 float(bp_old
.handle_right
.z
)
4170 spline_old_coords
.append(
4171 [coords
, left_handle_type
,
4172 right_handle_type
, left_handle_length
,
4173 right_handle_length
, left_handle_xyz
,
4177 for t
in range(len(sp
.bezier_points
)):
4178 bp
= sp
.bezier_points
4180 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4181 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4183 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4185 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4187 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4188 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4190 bp
[t
].handle_left_type
= "FREE"
4191 bp
[t
].handle_right_type
= "FREE"
4193 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4194 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4195 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4197 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4198 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4199 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4201 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4202 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4204 # Invert the non-cyclic splines designated above
4205 for i
in range(len(splines_to_invert
)):
4206 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4208 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4209 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4210 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4212 bpy
.ops
.curve
.switch_direction()
4214 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4216 # Keep selected the first vert of each spline
4217 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4218 for i
in range(len(self
.main_curve
.data
.splines
)):
4219 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4220 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4222 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4223 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4226 bp
.select_control_point
= True
4227 bp
.select_right_handle
= True
4228 bp
.select_left_handle
= True
4230 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4234 def invoke(self
, context
, event
):
4235 self
.main_curve
= bpy
.context
.object
4237 # Check if all curves are Bezier, and detect which ones are cyclic
4238 self
.cyclic_splines
= []
4239 for i
in range(len(self
.main_curve
.data
.splines
)):
4240 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4241 self
.report({'WARNING'}, "All splines must be Bezier type")
4243 return {'CANCELLED'}
4245 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4246 self
.cyclic_splines
.append(i
)
4248 self
.execute(context
)
4249 self
.report({'INFO'}, "First points have been set")
4254 # Add-ons Preferences Update Panel
4256 # Define Panel classes for updating
4258 VIEW3D_PT_tools_SURFSK_mesh
,
4259 VIEW3D_PT_tools_SURFSK_curve
4263 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4264 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4265 newCurve
.dimensions
= '3D'
4266 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4269 if type == 'GPensil':
4271 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4274 CurveObject
.location
= pencil
.location
4275 CurveObject
.rotation_euler
= pencil
.rotation_euler
4276 CurveObject
.scale
= pencil
.scale
4277 elif type == 'Annotation':
4279 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4282 CurveObject
.location
= (0.0, 0.0, 0.0)
4283 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4284 CurveObject
.scale
= (1.0, 1.0, 1.0)
4287 for i
, _stroke
in enumerate(strokes
):
4288 stroke_points
= strokes
[i
].points
4289 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4290 for point
in stroke_points
]
4291 points_to_add
= len(data_list
)-1
4294 for point
in data_list
:
4295 flat_list
.extend(point
)
4297 spline
= newCurve
.splines
.new(type='BEZIER')
4298 spline
.bezier_points
.add(points_to_add
)
4299 spline
.bezier_points
.foreach_set("co", flat_list
)
4301 for point
in spline
.bezier_points
:
4302 point
.handle_left_type
="AUTO"
4303 point
.handle_right_type
="AUTO"
4310 def update_panel(self
, context
):
4311 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4313 for panel
in panels
:
4314 if "bl_rna" in panel
.__dict
__:
4315 bpy
.utils
.unregister_class(panel
)
4317 for panel
in panels
:
4318 category
= context
.preferences
.addons
[__name__
].preferences
.category
4319 if category
!= 'Tool':
4320 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4322 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4323 panel
.bl_category
= 'Edit'
4324 raise ValueError("You can not install add-ons in the Tool panel")
4325 bpy
.utils
.register_class(panel
)
4327 except Exception as e
:
4328 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4331 def makeMaterial(name
, diffuse
):
4333 if name
in bpy
.data
.materials
:
4334 material
= bpy
.data
.materials
[name
]
4335 material
.diffuse_color
= diffuse
4337 material
= bpy
.data
.materials
.new(name
)
4338 material
.diffuse_color
= diffuse
4342 def update_mesh(self
, context
):
4344 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4345 bpy
.ops
.object.select_all(action
='DESELECT')
4346 bpy
.context
.view_layer
.update()
4347 global global_mesh_object
4348 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4349 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4350 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4352 print("Select mesh object")
4354 def update_gpencil(self
, context
):
4356 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4357 bpy
.ops
.object.select_all(action
='DESELECT')
4358 bpy
.context
.view_layer
.update()
4359 global global_gpencil_object
4360 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4361 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4362 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4364 print("Select gpencil object")
4366 def update_curve(self
, context
):
4368 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4369 bpy
.ops
.object.select_all(action
='DESELECT')
4370 bpy
.context
.view_layer
.update()
4371 global global_curve_object
4372 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4373 bpy
.data
.objects
[global_curve_object
].select_set(True)
4374 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4376 print("Select curve object")
4378 def update_shade_smooth(self
, context
):
4380 global global_shade_smooth
4381 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4383 contex_mode
= bpy
.context
.mode
4385 if bpy
.ops
.object.mode_set
.poll():
4386 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4388 bpy
.ops
.object.select_all(action
='DESELECT')
4389 global global_mesh_object
4390 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4391 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4393 if global_shade_smooth
:
4394 bpy
.ops
.object.shade_smooth()
4396 bpy
.ops
.object.shade_flat()
4398 if contex_mode
== "EDIT_MESH":
4399 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4402 print("Select mesh object")
4405 class BsurfPreferences(AddonPreferences
):
4406 # this must match the addon name, use '__package__'
4407 # when defining this in a submodule of a python package.
4408 bl_idname
= __name__
4410 category
: StringProperty(
4411 name
="Tab Category",
4412 description
="Choose a name for the category of the panel",
4417 def draw(self
, context
):
4418 layout
= self
.layout
4422 col
.label(text
="Tab Category:")
4423 col
.prop(self
, "category", text
="")
4426 class BsurfacesProps(PropertyGroup
):
4427 SURFSK_guide
: EnumProperty(
4430 ('Annotation', 'Annotation', 'Annotation'),
4431 ('GPencil', 'GPencil', 'GPencil'),
4432 ('Curve', 'Curve', 'Curve')
4434 default
="Annotation"
4436 SURFSK_edges_U
: IntProperty(
4438 description
="Number of face-loops crossing the strokes",
4443 SURFSK_edges_V
: IntProperty(
4445 description
="Number of face-loops following the strokes",
4450 SURFSK_cyclic_cross
: BoolProperty(
4451 name
="Cyclic Cross",
4452 description
="Make cyclic the face-loops crossing the strokes",
4455 SURFSK_cyclic_follow
: BoolProperty(
4456 name
="Cyclic Follow",
4457 description
="Make cyclic the face-loops following the strokes",
4460 SURFSK_keep_strokes
: BoolProperty(
4461 name
="Keep strokes",
4462 description
="Keeps the sketched strokes or curves after adding the surface",
4465 SURFSK_automatic_join
: BoolProperty(
4466 name
="Automatic join",
4467 description
="Join automatically vertices of either surfaces "
4468 "generated by crosshatching, or from the borders of closed shapes",
4471 SURFSK_loops_on_strokes
: BoolProperty(
4472 name
="Loops on strokes",
4473 description
="Make the loops match the paths of the strokes",
4476 SURFSK_precision
: IntProperty(
4478 description
="Precision level of the surface calculation",
4483 SURFSK_mesh
: PointerProperty(
4484 name
="Mesh of BSurface",
4485 type=bpy
.types
.Object
,
4486 description
="Mesh of BSurface",
4489 SURFSK_gpencil
: PointerProperty(
4490 name
="GreasePencil object",
4491 type=bpy
.types
.Object
,
4492 description
="GreasePencil object",
4493 update
=update_gpencil
,
4495 SURFSK_curve
: PointerProperty(
4496 name
="Curve object",
4497 type=bpy
.types
.Object
,
4498 description
="Curve object",
4499 update
=update_curve
,
4501 SURFSK_shade_smooth
: BoolProperty(
4502 name
="Shade smooth",
4503 description
="Render and display faces smooth, using interpolated Vertex Normals",
4505 update
=update_shade_smooth
,
4509 MESH_OT_SURFSK_init
,
4510 MESH_OT_SURFSK_add_modifiers
,
4511 MESH_OT_SURFSK_add_surface
,
4512 MESH_OT_SURFSK_edit_surface
,
4513 GPENCIL_OT_SURFSK_add_strokes
,
4514 GPENCIL_OT_SURFSK_edit_strokes
,
4515 GPENCIL_OT_SURFSK_strokes_to_curves
,
4516 GPENCIL_OT_SURFSK_annotation_to_curves
,
4517 GPENCIL_OT_SURFSK_add_annotation
,
4518 CURVE_OT_SURFSK_edit_curve
,
4519 CURVE_OT_SURFSK_reorder_splines
,
4520 CURVE_OT_SURFSK_first_points
,
4527 bpy
.utils
.register_class(cls
)
4529 for panel
in panels
:
4530 bpy
.utils
.register_class(panel
)
4532 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4533 update_panel(None, bpy
.context
)
4536 for panel
in panels
:
4537 bpy
.utils
.unregister_class(panel
)
4540 bpy
.utils
.unregister_class(cls
)
4542 del bpy
.types
.Scene
.bsurfaces
4544 if __name__
== "__main__":