OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / misc / to_gpstroke.py

194 lines · python ref:
import bpy, bmesh
from ... utility import addon, operator_override, context_copy
from math import pi


DESC = """Convert sharp edges to grease pencil object
"""

class HOPS_OT_TO_GPSTROKE(bpy.types.Operator):
    bl_idname = 'hops.to_gpstroke'
    bl_label = 'Create Grease Pencil from mesh'
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
    bl_description = DESC

    modified : bpy.props.BoolProperty(
    name='Modified',
    description='Use modified mesh',
    default=True,
    )

    convert_faces : bpy.props.BoolProperty(
    name='Faces',
    description='Convert Faces into filled strokes',
    default=True,
    )

    thickness : bpy.props.IntProperty(
    name='Thickness',
    description='Stroke Thickness',
    default=5,
    min=0,
    )

    stroke_offset : bpy.props.FloatProperty(
    name='Offset',
    description='Stroke offset',
    default=0.01,
    min=0
    )

    sharpness : bpy.props.FloatProperty(
    name='Sharpness',
    description='Face angle above which edge is considered sharp',
    default=pi/6,
    min=0,
    max = pi,
    subtype='ANGLE',
    )

    stroke_color : bpy.props.FloatVectorProperty(
    name='Stroke Color',
    description='Stroke Color',
    size=4,
    default= [0,0,0,1],
    min=0,
    max=1,
    subtype='COLOR'
    )

    fill_color : bpy.props.FloatVectorProperty(
    name='Fill Color',
    description='Fill Color',
    size=4,
    default= [1,1,1,1],
    min=0,
    max=1,
    subtype='COLOR'
    )

    hide_original : bpy.props.BoolProperty(
    name='Hide Original',
    description='Hide original object(s)',
    default=True,
    )


    object_types = frozenset((
        'MESH', 'CURVE', 'FONT', 'SURFACE', 'META'
    ))

    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'

    def draw(self, context):
        self.layout.prop(self, 'modified')
        self.layout.prop(self, 'convert_faces')
        self.layout.prop(self, 'thickness')
        self.layout.prop(self, 'stroke_offset')
        self.layout.prop(self, 'sharpness')
        self.layout.prop(self, 'stroke_color')

        if self.convert_faces:
            self.layout.prop(self, 'fill_color')

        self.layout.prop(self, 'hide_original')

    def invoke(self, context, event):
        self.notify = lambda val, sub='': bpy.ops.hops.display_notification(info=val, subtext=sub) if addon.preference().ui.Hops_extra_info else lambda val, sub=None: None
        self.selection = {o.name for o in context.selected_objects if o.type in self.object_types}
        self.active_obj_name = context.active_object.name

        if not self.selection:
            msg = 'No meshes in selection'
            self.notify('CANCELLED', msg)
            self.report({'INFO'}, msg)
            return {'CANCELLED'}

        return self.execute(context)

    def execute(self, context):
        created_objects = []

        bpy.ops.object.select_all(action='DESELECT')

        for name in self.selection:
            obj = bpy.data.objects[name]

            if self.modified:
                obj_eval = obj.evaluated_get(context.evaluated_depsgraph_get())
                mesh = obj_eval.data if obj.type == 'MESH' else obj_eval.to_mesh()
                bm = self.create_bmesh_data(mesh)
                obj_eval.to_mesh_clear()

            else:
                mesh = obj.data if obj.type == 'MESH' else obj.to_mesh()
                bm = self.create_bmesh_data(mesh)

            if not bm: continue

            new_data = bpy.data.meshes.new('Stroke')
            new_obj = bpy.data.objects.new('Stroke', new_data)
            new_obj.matrix_world = obj.matrix_world
            bm.to_mesh(new_data)

            context.collection.objects.link(new_obj)
            new_obj.select_set(True)

            override = context_copy(context)
            override['object'] = new_obj
            override['active_object'] = new_obj
            mod = new_obj.modifiers.new(type='TRIANGULATE', name='tri')
            mod.min_vertices = 5

            operator_override(context, bpy.ops.object.convert, override, target='GPENCIL', seams=True, faces=self.convert_faces, thickness=self.thickness, offset=self.stroke_offset)

            stroke = context.active_object
            created_objects.append(stroke)

            for slot in stroke.material_slots:
                slot.material.grease_pencil.color = self.stroke_color
                slot.material.grease_pencil.fill_color = self.fill_color

            if self.hide_original: obj.hide_set(True)

        if not created_objects:
            for name in self.selection:
                obj = bpy.data.objects[name]
                obj.select_set(True)

            context.view_layer.objects.active = bpy.data.objects[self.active_obj_name]
            msg ='No valid geometry in selection or sharpenss angle is too high'
            self.notify('CANCELLED', msg)
            self.report({'INFO'}, msg)

            return {'FINISHED'}

        for stroke in created_objects:
            stroke.select_set(True)

        msg = f'Created {len(created_objects)}/{len(self.selection)} strokes'
        self.notify('TO_STROKE', msg)

        self.report({'INFO'}, msg)
        return {'FINISHED'}

    def create_bmesh_data(self, input):
        bm = bmesh.new()
        bm.from_mesh(input)
        counter = 0

        for edge in bm.edges:
            flag = not edge.smooth or edge.calc_face_angle(4) >= self.sharpness
            edge.seam = flag
            counter += flag

        if self.convert_faces:
            if not counter and not bm.faces: return False

        else:
            if not counter: return False

        return bm