1 # SPDX-FileCopyrightText: 2022-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 # (c) Alessandro Zomparelli #
9 # http://www.co-de-it.com/ #
11 # ############################################################################ #
15 from bpy
.types
import Operator
16 from bpy
.props
import (
24 from bpy
.types
import (
31 from mathutils
import Vector
35 update_curve_from_pydata
,
37 convert_object_to_mesh
,
45 def anim_curve_active(self
, context
):
47 props
= ob
.tissue_to_curve
50 if not ob
.tissue
.bool_lock
:
51 bpy
.ops
.object.tissue_convert_to_curve_update()
55 class tissue_to_curve_prop(PropertyGroup
):
56 object : PointerProperty(
57 type=bpy
.types
.Object
,
59 description
="Source object",
60 update
= anim_curve_active
62 bool_smooth
: BoolProperty(
63 name
="Smooth Shading",
65 description
="Output faces with smooth shading rather than flat shaded",
66 update
= anim_curve_active
68 bool_lock
: BoolProperty(
70 description
="Prevent automatic update on settings changes or if other objects have it in the hierarchy",
72 update
= anim_curve_active
74 bool_dependencies
: BoolProperty(
75 name
="Update Dependencies",
76 description
="Automatically updates source object as well, when possible",
78 update
= anim_curve_active
80 bool_run
: BoolProperty(
81 name
="Animatable Curve",
82 description
="Automatically recompute the conversion when the frame is changed",
85 use_modifiers
: BoolProperty(
88 description
="Automatically apply Modifiers and Shape Keys",
89 update
= anim_curve_active
91 subdivision_mode
: EnumProperty(
95 ('INNER', "Inner", "")
98 name
="Subdivided Edges",
99 update
= anim_curve_active
101 use_endpoint_u
: BoolProperty(
104 description
="Make all open nurbs curve meet the endpoints",
105 update
= anim_curve_active
107 clean_distance
: FloatProperty(
108 name
="Merge Distance", default
=0, min=0, soft_max
=10,
109 description
="Merge Distance",
110 update
= anim_curve_active
112 nurbs_order
: IntProperty(
113 name
="Order", default
=4, min=2, max=6,
114 description
="Nurbs order",
115 update
= anim_curve_active
117 system
: IntProperty(
118 name
="System", default
=0, min=0,
119 description
="Particle system index",
120 update
= anim_curve_active
122 bounds_selection
: EnumProperty(
125 ('BOUNDS', "Boundaries", ""),
126 ('INNER', "Inner", "")
129 name
="Boundary Selection",
130 update
= anim_curve_active
132 periodic_selection
: EnumProperty(
135 ('OPEN', "Open", ""),
136 ('CLOSED', "Closed", "")
139 name
="Periodic Selection",
140 update
= anim_curve_active
142 spline_type
: EnumProperty(
144 ('POLY', "Poly", ""),
145 ('BEZIER', "Bezier", ""),
146 ('NURBS', "NURBS", "")
150 update
= anim_curve_active
154 ('LOOPS', "Loops", ""),
155 ('EDGES', "Edges", ""),
156 ('PARTICLES', "Particles", "")
159 name
="Conversion Mode",
160 update
= anim_curve_active
162 vertex_group
: StringProperty(
163 name
="Radius", default
='',
164 description
="Vertex Group used for variable radius",
165 update
= anim_curve_active
167 invert_vertex_group
: BoolProperty(default
=False,
168 description
='Inverte the value of the Vertex Group',
169 update
= anim_curve_active
171 vertex_group_factor
: FloatProperty(
176 description
="Depth bevel factor to use for zero vertex group influence",
177 update
= anim_curve_active
179 only_sharp
: BoolProperty(
181 name
="Only Sharp Edges",
182 description
='Convert only Sharp edges',
183 update
= anim_curve_active
185 pattern_depth
: FloatProperty(
190 description
="Displacement pattern depth",
191 update
= anim_curve_active
193 pattern_offset
: FloatProperty(
198 description
="Displacement pattern offset",
199 update
= anim_curve_active
201 pattern0
: IntProperty(
206 description
="Pattern step 0",
207 update
= anim_curve_active
209 pattern1
: IntProperty(
214 description
="Pattern step 1",
215 update
= anim_curve_active
218 class tissue_convert_to_curve(Operator
):
219 bl_idname
= "object.tissue_convert_to_curve"
220 bl_label
= "Tissue Convert to Curve"
221 bl_description
= "Convert selected mesh to Curve object"
222 bl_options
= {'REGISTER', 'UNDO'}
224 object : StringProperty(
226 description
="Source object",
229 bool_smooth
: BoolProperty(
230 name
="Smooth Shading",
232 description
="Output faces with smooth shading rather than flat shaded"
234 use_modifiers
: BoolProperty(
235 name
="Use Modifiers",
237 description
="Automatically apply Modifiers and Shape Keys"
239 subdivision_mode
: EnumProperty(
242 ('CAGE', "Cage", ""),
243 ('INNER', "Inner", "")
246 name
="Subdivided Edges"
248 use_endpoint_u
: BoolProperty(
251 description
="Make all open nurbs curve meet the endpoints"
253 nurbs_order
: IntProperty(
254 name
="Order", default
=4, min=2, max=6,
255 description
="Nurbs order"
257 system
: IntProperty(
258 name
="System", default
=0, min=0,
259 description
="Particle system index"
261 clean_distance
: FloatProperty(
262 name
="Merge Distance", default
=0, min=0, soft_max
=10,
263 description
="Merge Distance"
265 spline_type
: EnumProperty(
267 ('POLY', "Poly", ""),
268 ('BEZIER', "Bezier", ""),
269 ('NURBS', "NURBS", "")
274 bounds_selection
: EnumProperty(
277 ('BOUNDS', "Boundaries", ""),
278 ('INNER', "Inner", "")
281 name
="Boundary Selection"
283 periodic_selection
: EnumProperty(
286 ('OPEN', "Open", ""),
287 ('CLOSED', "Closed", "")
290 name
="Periodic Selection"
294 ('LOOPS', "Loops", ""),
295 ('EDGES', "Edges", ""),
296 ('PARTICLES', "Particles", "")
299 name
="Conversion Mode"
301 vertex_group
: StringProperty(
302 name
="Radius", default
='',
303 description
="Vertex Group used for variable radius"
305 invert_vertex_group
: BoolProperty(default
=False,
306 description
='Inverte the value of the Vertex Group'
308 vertex_group_factor
: FloatProperty(
313 description
="Depth bevel factor to use for zero vertex group influence"
315 only_sharp
: BoolProperty(
317 name
="Only Sharp Edges",
318 description
='Convert only Sharp edges'
320 pattern_depth
: FloatProperty(
325 description
="Displacement pattern depth"
327 pattern_offset
: FloatProperty(
332 description
="Displacement pattern offset"
334 pattern0
: IntProperty(
339 description
="Pattern step 0"
341 pattern1
: IntProperty(
346 description
="Pattern step 1"
350 def poll(cls
, context
):
352 #bool_tessellated = context.object.tissue_tessellate.generator != None
354 return ob
.type in ('MESH','CURVE','SURFACE','FONT') and ob
.mode
== 'OBJECT'# and bool_tessellated
358 def invoke(self
, context
, event
):
359 self
.object = context
.object.name
360 return context
.window_manager
.invoke_props_dialog(self
)
362 def draw(self
, context
):
364 ob0
= bpy
.data
.objects
[self
.object]
365 #props = ob.tissue_to_curve
367 col
= layout
.column(align
=True)
368 row
= col
.row(align
=True)
369 #row.label(text='Object: ' + self.object)
370 #row.prop_search(self, "object", context.scene, "objects")
371 #row.prop(self, "use_modifiers")#, icon='MODIFIER', text='')
373 col
.label(text
='Conversion Mode:')
374 row
= col
.row(align
=True)
376 self
, "mode", text
="Conversion Mode", icon
='NONE', expand
=True,
377 slider
=False, toggle
=False, icon_only
=False, event
=False,
378 full_event
=False, emboss
=True, index
=-1)
379 if self
.mode
== 'PARTICLES':
381 col
.prop(self
, "system")
383 if self
.mode
in ('LOOPS', 'EDGES'):
384 row
= col
.row(align
=True)
385 row
.prop(self
, "use_modifiers")
386 col2
= row
.column(align
=True)
387 if self
.use_modifiers
:
388 col2
.prop(self
, "subdivision_mode", text
='', icon
='NONE', expand
=False,
389 slider
=True, toggle
=False, icon_only
=False, event
=False,
390 full_event
=False, emboss
=True, index
=-1)
392 for m
in bpy
.data
.objects
[self
.object].modifiers
:
393 if m
.type in ('SUBSURF','MULTIRES'): col2
.enabled
= True
395 row
= col
.row(align
=True)
396 row
.label(text
='Filter Edges:')
397 col2
= row
.column(align
=True)
398 col2
.prop(self
, "bounds_selection", text
='', icon
='NONE', expand
=False,
399 slider
=True, toggle
=False, icon_only
=False, event
=False,
400 full_event
=False, emboss
=True, index
=-1)
401 col2
.prop(self
, 'only_sharp')
403 if self
.mode
== 'LOOPS':
404 row
= col
.row(align
=True)
405 row
.label(text
='Filter Loops:')
406 row
.prop(self
, "periodic_selection", text
='', icon
='NONE', expand
=False,
407 slider
=True, toggle
=False, icon_only
=False, event
=False,
408 full_event
=False, emboss
=True, index
=-1)
410 col
.label(text
='Spline Type:')
411 row
= col
.row(align
=True)
413 self
, "spline_type", text
="Spline Type", icon
='NONE', expand
=True,
414 slider
=False, toggle
=False, icon_only
=False, event
=False,
415 full_event
=False, emboss
=True, index
=-1)
416 if self
.spline_type
== 'NURBS':
418 col
.label(text
='Nurbs splines:')
419 row
= col
.row(align
=True)
420 row
.prop(self
, "use_endpoint_u")
421 row
.prop(self
, "nurbs_order")
423 col
.prop(self
, "bool_smooth")
424 if ob0
.type == 'MESH' and self
.mode
!= 'PARTICLES':
426 col
.label(text
='Variable Radius:')
427 row
= col
.row(align
=True)
428 row
.prop_search(self
, 'vertex_group', ob0
, "vertex_groups", text
='')
429 row
.prop(self
, "invert_vertex_group", text
="", toggle
=True, icon
='ARROW_LEFTRIGHT')
430 row
.prop(self
, "vertex_group_factor")
432 col
.label(text
='Clean curves:')
433 col
.prop(self
, "clean_distance")
435 col
.label(text
='Displacement Pattern:')
436 row
= col
.row(align
=True)
437 row
.prop(self
, "pattern0")
438 row
.prop(self
, "pattern1")
439 row
= col
.row(align
=True)
440 row
.prop(self
, "pattern_depth")
441 row
.prop(self
, "pattern_offset")
443 def execute(self
, context
):
444 ob
= context
.active_object
446 crv
= bpy
.data
.curves
.new(ob
.name
+ '_Curve', type='CURVE')
447 crv
.dimensions
= '3D'
448 new_ob
= bpy
.data
.objects
.new(ob
.name
+ '_Curve', crv
)
449 bpy
.context
.collection
.objects
.link(new_ob
)
450 context
.view_layer
.objects
.active
= new_ob
452 new_ob
.select_set(True)
454 new_ob
.matrix_world
= ob
.matrix_world
456 new_ob
.tissue
.tissue_type
= 'TO_CURVE'
457 new_ob
.tissue
.bool_lock
= True
459 props
= new_ob
.tissue_to_curve
461 props
.use_modifiers
= self
.use_modifiers
462 props
.subdivision_mode
= self
.subdivision_mode
463 props
.clean_distance
= self
.clean_distance
464 props
.spline_type
= self
.spline_type
465 props
.mode
= self
.mode
466 props
.use_endpoint_u
= self
.use_endpoint_u
467 props
.nurbs_order
= self
.nurbs_order
468 props
.vertex_group
= self
.vertex_group
469 props
.vertex_group_factor
= self
.vertex_group_factor
470 props
.invert_vertex_group
= self
.invert_vertex_group
471 props
.bool_smooth
= self
.bool_smooth
472 props
.system
= self
.system
473 props
.periodic_selection
= self
.periodic_selection
474 props
.bounds_selection
= self
.bounds_selection
475 props
.only_sharp
= self
.only_sharp
476 props
.pattern0
= self
.pattern0
477 props
.pattern1
= self
.pattern1
478 props
.pattern_depth
= self
.pattern_depth
479 props
.pattern_offset
= self
.pattern_offset
481 new_ob
.tissue
.bool_lock
= False
483 bpy
.ops
.object.tissue_convert_to_curve_update()
487 class tissue_convert_to_curve_update(Operator
):
488 bl_idname
= "object.tissue_convert_to_curve_update"
489 bl_label
= "Tissue Update Curve"
490 bl_description
= "Update Curve object"
491 bl_options
= {'REGISTER', 'UNDO'}
494 def poll(cls
, context
):
497 bool_curve
= ob
.tissue_to_curve
.object != None
498 return ob
.type == 'CURVE' and ob
.mode
== 'OBJECT' and bool_curve
502 def execute(self
, context
):
503 start_time
= time
.time()
506 props
= ob
.tissue_to_curve
508 if props
.mode
== 'PARTICLES':
509 eval_ob
= ob0
.evaluated_get(context
.evaluated_depsgraph_get())
510 system_id
= min(props
.system
, len(eval_ob
.particle_systems
))
511 psystem
= eval_ob
.particle_systems
[system_id
]
512 ob
.data
.splines
.clear()
513 particles
= psystem
.particles
514 for id,p
in enumerate(particles
):
515 s
= ob
.data
.splines
.new('POLY')
516 if psystem
.settings
.type == 'HAIR':
517 n_pts
= len(p
.hair_keys
)
519 p
.hair_keys
.foreach_get('co',pts
)
520 co
= np
.array(pts
).reshape((-1,3))
522 n_pts
= 2**psystem
.settings
.display_step
+ 1
524 for i
in range(n_pts
):
525 vec
= psystem
.co_hair(eval_ob
, particle_no
=id,step
=i
)
526 vec
= ob0
.matrix_world
.inverted() @ vec
529 w
= np
.ones(n_pts
).reshape((n_pts
,1))
530 co
= np
.concatenate((co
,w
),axis
=1).reshape((n_pts
*4))
531 s
.points
.add(n_pts
-1)
532 s
.points
.foreach_set('co',co
)
536 ob0
= convert_object_to_mesh(ob0
, apply_modifiers
=props
.use_modifiers
)
538 n_verts
= len(me
.vertices
)
539 verts
= [0]*n_verts
*3
540 me
.vertices
.foreach_get('co',verts
)
541 verts
= np
.array(verts
).reshape((-1,3))
543 normals
= [0]*n_verts
*3
544 me
.vertices
.foreach_get('normal',normals
)
545 normals
= np
.array(normals
).reshape((-1,3))
546 #tilt = np.degrees(np.arcsin(normals[:,2]))
547 #tilt = np.arccos(normals[:,2])/2
549 verts
= np
.array(verts
).reshape((-1,3))
550 if props
.mode
in ('LOOPS','EDGES'):
553 bm
.verts
.ensure_lookup_table()
554 bm
.edges
.ensure_lookup_table()
555 bm
.faces
.ensure_lookup_table()
556 todo_edges
= list(bm
.edges
)
557 if props
.use_modifiers
and props
.subdivision_mode
!= 'ALL':
558 me0
, subs
= get_mesh_before_subs(_ob0
)
559 n_edges0
= len(me0
.edges
)
560 bpy
.data
.meshes
.remove(me0
)
561 if props
.subdivision_mode
== 'CAGE':
562 todo_edges
= todo_edges
[:n_edges0
*(2**subs
)]
563 elif props
.subdivision_mode
== 'INNER':
564 todo_edges
= todo_edges
[n_edges0
*(2**subs
):]
570 edge
= me
.edges
[e
.index
]
571 if edge
.use_edge_sharp
:
572 _todo_edges
.append(e
)
573 sharp_verts
.append(edge
.vertices
[0])
574 sharp_verts
.append(edge
.vertices
[1])
575 todo_edges
= _todo_edges
577 if props
.bounds_selection
== 'BOUNDS': todo_edges
= [e
for e
in todo_edges
if len(e
.link_faces
)<2]
578 elif props
.bounds_selection
== 'INNER': todo_edges
= [e
for e
in todo_edges
if len(e
.link_faces
)>1]
580 if props
.mode
== 'EDGES':
581 ordered_points
= [[e
.verts
[0].index
, e
.verts
[1].index
] for e
in todo_edges
]
582 elif props
.mode
== 'LOOPS':
583 vert_loops
, edge_loops
= loops_from_bmesh(todo_edges
)
586 for loop
in vert_loops
:
589 if v
.index
in sharp_verts
:
590 loop_points
.append(v
.index
)
592 if len(loop_points
)>1:
593 ordered_points
.append(loop_points
)
595 if len(loop_points
)>1:
596 ordered_points
.append(loop_points
)
597 #ordered_points = [[v.index for v in loop if v.index in sharp_verts] for loop in vert_loops]
599 ordered_points
= [[v
.index
for v
in loop
] for loop
in vert_loops
]
600 if props
.periodic_selection
== 'CLOSED':
601 ordered_points
= [points
for points
in ordered_points
if points
[0] == points
[-1]]
602 elif props
.periodic_selection
== 'OPEN':
603 ordered_points
= [points
for points
in ordered_points
if points
[0] != points
[-1]]
606 ordered_points
= find_curves(edges
, n_verts
)
608 bpy
.data
.objects
.remove(ob0
)
612 weight
= get_weight_numpy(ob0
.vertex_groups
[props
.vertex_group
], n_verts
)
613 if props
.invert_vertex_group
: weight
= 1-weight
614 fact
= props
.vertex_group_factor
616 weight
= weight
*(1-fact
) + fact
623 for points in ordered_points:
624 if points[0] == points[-1]: # Closed curve
625 pts0 = [points[-1]] + points[:-1] # i-1
627 pts2 = points[1:] + [points[0]] # 1+1
629 pts0 = [points[0]] + points[:-1] # i-1
631 pts2 = points[1:] + [points[-1]] # i+1
633 for i0, i1, i2 in zip(pts0, pts1, pts2):
634 pt0 = Vector(verts[i0])
635 pt1 = Vector(verts[i1])
636 pt2 = Vector(verts[i2])
637 tan1 = (pt1-pt0).normalized()
638 tan2 = (pt2-pt1).normalized()
639 vec_tan = -(tan1 + tan2).normalized()
640 vec2 = vec_tan.cross(Vector((0,0,1)))
641 vec_z = vec_tan.cross(vec2)
643 if vec_z.length == 0:
645 ang = vec_z.angle(nor)
646 if nor[2] < 0: ang = 2*pi-ang
647 #if vec_tan[0] > vec_tan[1] and nor[0]>0: ang = -ang
648 #if vec_tan[0] > vec_tan[2] and nor[0]>0: ang = -ang
649 #if vec_tan[0] < vec_tan[1] and nor[1]>0: ang = -ang
650 #if nor[0]*nor[1]*nor[2] < 0: ang = -ang
651 if nor[2] == 0: ang = -5*pi/4
652 #ang = max(ang, np.arccos(nor[2]))
653 curve_tilt.append(ang)
654 #curve_tilt.append(np.arccos(nor[2]))
655 tilt.append(curve_tilt)
657 depth
= props
.pattern_depth
658 offset
= props
.pattern_offset
659 pattern
= [props
.pattern0
,props
.pattern1
]
660 update_curve_from_pydata(ob
.data
, verts
, normals
, weight
, ordered_points
, merge_distance
=props
.clean_distance
, pattern
=pattern
, depth
=depth
, offset
=offset
)
663 bpy
.data
.objects
.remove(ob0
)
664 for s
in ob
.data
.splines
:
665 s
.type = props
.spline_type
666 if s
.type == 'NURBS':
667 s
.use_endpoint_u
= props
.use_endpoint_u
668 s
.order_u
= props
.nurbs_order
669 ob
.data
.splines
.update()
670 if not props
.bool_smooth
: bpy
.ops
.object.shade_flat()
672 end_time
= time
.time()
673 print('Tissue: object "{}" converted to Curve in {:.4f} sec'.format(ob
.name
, end_time
-start_time
))
678 class TISSUE_PT_convert_to_curve(Panel
):
679 bl_space_type
= 'PROPERTIES'
680 bl_region_type
= 'WINDOW'
682 bl_label
= "Tissue Convert to Curve"
683 bl_options
= {'DEFAULT_CLOSED'}
686 def poll(cls
, context
):
688 #bool_curve = context.object.tissue_to_curve.object != None
690 return ob
.type == 'CURVE' and ob
.tissue
.tissue_type
== 'TO_CURVE'
694 def draw(self
, context
):
696 props
= ob
.tissue_to_curve
699 #layout.use_property_split = True
700 #layout.use_property_decorate = False
701 col
= layout
.column(align
=True)
702 row
= col
.row(align
=True)
703 #col.operator("object.tissue_convert_to_curve_update", icon='FILE_REFRESH', text='Refresh')
704 row
.operator("object.tissue_update_tessellate_deps", icon
='FILE_REFRESH', text
='Refresh') ####
705 lock_icon
= 'LOCKED' if ob
.tissue
.bool_lock
else 'UNLOCKED'
706 #lock_icon = 'PINNED' if props.bool_lock else 'UNPINNED'
707 deps_icon
= 'LINKED' if ob
.tissue
.bool_dependencies
else 'UNLINKED'
708 row
.prop(ob
.tissue
, "bool_dependencies", text
="", icon
=deps_icon
)
709 row
.prop(ob
.tissue
, "bool_lock", text
="", icon
=lock_icon
)
710 col2
= row
.column(align
=True)
711 col2
.prop(ob
.tissue
, "bool_run", text
="",icon
='TIME')
712 col2
.enabled
= not ob
.tissue
.bool_lock
715 row
= col
.row(align
=True)
716 row
.prop_search(props
, "object", context
.scene
, "objects")
717 row
.prop(props
, "use_modifiers", icon
='MODIFIER', text
='')
719 col
.label(text
='Conversion Mode:')
720 row
= col
.row(align
=True)
722 props
, "mode", icon
='NONE', expand
=True,
723 slider
=False, toggle
=False, icon_only
=False, event
=False,
724 full_event
=False, emboss
=True, index
=-1)
725 if props
.mode
== 'PARTICLES':
727 col
.prop(props
, "system")
730 if props
.mode
in ('LOOPS', 'EDGES'):
731 row
= col
.row(align
=True)
732 row
.prop(props
, "use_modifiers")
733 col2
= row
.column(align
=True)
734 if props
.use_modifiers
:
735 col2
.prop(props
, "subdivision_mode", text
='', icon
='NONE', expand
=False,
736 slider
=True, toggle
=False, icon_only
=False, event
=False,
737 full_event
=False, emboss
=True, index
=-1)
739 for m
in props
.object.modifiers
:
740 if m
.type in ('SUBSURF','MULTIRES'): col2
.enabled
= True
742 row
= col
.row(align
=True)
743 row
.label(text
='Filter Edges:')
744 col2
= row
.column(align
=True)
745 col2
.prop(props
, "bounds_selection", text
='', icon
='NONE', expand
=False,
746 slider
=True, toggle
=False, icon_only
=False, event
=False,
747 full_event
=False, emboss
=True, index
=-1)
748 col2
.prop(props
, 'only_sharp')
750 if props
.mode
== 'LOOPS':
751 row
= col
.row(align
=True)
752 row
.label(text
='Filter Loops:')
753 row
.prop(props
, "periodic_selection", text
='', icon
='NONE', expand
=False,
754 slider
=True, toggle
=False, icon_only
=False, event
=False,
755 full_event
=False, emboss
=True, index
=-1)
758 col
.label(text
='Spline Type:')
759 row
= col
.row(align
=True)
761 props
, "spline_type", text
="Spline Type", icon
='NONE', expand
=True,
762 slider
=False, toggle
=False, icon_only
=False, event
=False,
763 full_event
=False, emboss
=True, index
=-1)
764 if props
.spline_type
== 'NURBS':
766 col
.label(text
='Nurbs Splines:')
767 row
= col
.row(align
=True)
768 row
.prop(props
, "use_endpoint_u")
769 row
.prop(props
, "nurbs_order")
771 col
.prop(props
, "bool_smooth")
772 if props
.object.type == 'MESH':
774 col
.label(text
='Variable Radius:')
775 row
= col
.row(align
=True)
776 row
.prop_search(props
, 'vertex_group', props
.object, "vertex_groups", text
='')
777 row
.prop(props
, "invert_vertex_group", text
="", toggle
=True, icon
='ARROW_LEFTRIGHT')
778 row
.prop(props
, "vertex_group_factor")
780 col
.label(text
='Clean Curves:')
781 col
.prop(props
, "clean_distance")
783 col
.label(text
='Displacement Pattern:')
784 row
= col
.row(align
=True)
785 row
.prop(props
, "pattern0")
786 row
.prop(props
, "pattern1")
787 row
= col
.row(align
=True)
788 row
.prop(props
, "pattern_depth")
789 row
.prop(props
, "pattern_offset")