← Blender.Today
  • 1
  • Published 8 Sep 2022

Grease Pencil Extras addon

Sidebar (N) addon with the following utility functionality:

  • Converge selection to selected point
  • Align selected points horizontally
  • Align selected points vertically
  • Adjust selected stroke(s) hardness / strength / pressure
bl_info = {
    "name": "Grease Pencil Extras",
    "author": "pongbuster",
    "version": (1, 0),
    "blender": (2, 90, 0),
    "location": "View3D > Sidebar (N)",
    "description": "A collection of utilities to align grease pencil stroke points and adjust stroke hardness.",
    "warning": "",
    "doc_url": "",
    "category": "Grease Pencil",
}

import bpy
from bpy_extras import view3d_utils
from mathutils import Vector
from bpy.props import IntProperty

selected_points = []

def init_selected(context):
    gp = context.active_object

    if gp.type != 'GPENCIL':
        return

    selected_points.clear()

    for lr in gp.data.layers:
        for fr in lr.frames:
            if fr.frame_number == context.scene.frame_current:
                for s in fr.strokes:
                    for p in s.points:
                        if p.select:
                            selected_points.append(p)

class hardnessOperator(bpy.types.Operator):
    """Middle mouse to adjust selected Strokes' hardness.
Hold CTRL to adjust radius/strength.
Hold SHIFT to adjust pressure.
Left click to apply.
"""

    bl_idname = "stroke.hardness"
    bl_label = "Stroke Hardness"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(self, context):
        return (context.mode == 'SCULPT_GPENCIL' or context.mode == 'EDIT_GPENCIL')

    def modal(self, context, event):
        global selected_points

        if event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE":
            incr = -0.01 if event.type == "WHEELDOWNMOUSE" else 0.01

            if event.shift:
                incr *= 2
                for p in selected_points:
                    p.strength += incr
            elif event.ctrl:
                incr *= 100
                for p in selected_points:
                    p.pressure += incr
            else:    
                for lr in context.active_object.data.layers:
                    for fr in lr.frames:
                        if fr.frame_number == context.scene.frame_current:
                            for s in fr.strokes:
                                if s.select:
                                    s.hardness += incr
                                    context.area.header_text_set("Hardness: %.4f" % s.hardness)

        elif event.type == "LEFTMOUSE":
            context.area.header_text_set(None)
            context.window.cursor_modal_restore()
            return {'FINISHED'}

        return {'RUNNING_MODAL'}    

    def execute(self, context):
        init_selected(context)    
        context.window.cursor_modal_set("SCROLL_Y")
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}

class convergeOperator(bpy.types.Operator):
    arg: bpy.props.StringProperty()

    bl_idname = "stroke.converge"
    bl_label = "Converge Selection"
    bl_options = {'REGISTER', 'UNDO'}

    selectedPoint = None
    align : IntProperty(default=0)

    @classmethod
    def description(cls, context, properties):
        txt = "Converge"

        if properties.align == 1:
            txt = "Align horizontally"
        elif properties.align == 2:
            txt = "Align vertically"

        txt += """ selected points to the closest selected point from mouse.
Left click to apply. Right click to cancel.
        """

        return txt

    @classmethod
    def poll(self, context):
        return (context.mode == 'SCULPT_GPENCIL' or context.mode == 'EDIT_GPENCIL')

    def modal(self, context, event):
        global selected_points

        if event.type == "MOUSEMOVE":
            pos = view3d_utils.region_2d_to_location_3d(context.region, context.space_data.region_3d, 
                (event.mouse_region_x, event.mouse_region_y), (0,0,0))

            for p in selected_points:
                v = Vector((pos[0] - p.co[0], 0, pos[2] - p.co[2]))
                self.selectedPoint = None
                if v.length < 0.04:
                    self.selectedPoint = p
                    break

            if self.selectedPoint:
               context.window.cursor_modal_set("CROSSHAIR")
            else:
                context.window.cursor_modal_set("PAINT_CROSS")

        elif event.type == "LEFTMOUSE":
            context.window.cursor_modal_restore()
            context.window.cursor_modal_restore()

            if self.selectedPoint:
                for p in selected_points:
                    if self.align == 0 or self.align == 1:
                        p.co[0] = self.selectedPoint.co[0]
                    if self.align == 0 or self.align == 2:
                        p.co[2] = self.selectedPoint.co[2]

                return {'FINISHED'}
            return {'CANCELLED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            context.window.cursor_modal_restore()

            return {'CANCELLED'}

        return {'RUNNING_MODAL'}    

    def execute(self, context):
        init_selected(context)    
        context.window.cursor_modal_set("PAINT_CROSS")
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}

class PGP_PT_sidebarPanel(bpy.types.Panel):
    bl_label = "Grease Pencil Extras"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Grease Pencil"

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row.operator('stroke.converge', text = "Converge selection" ).align = 0
        row = layout.row()
        row.operator('stroke.converge', text = "Align Horizontally" ).align = 1
        row = layout.row()
        row.operator('stroke.converge', text = "Align Vertically" ).align = 2
        row = layout.row()
        row.operator('stroke.hardness', text = "Stroke Hardness")


# Class list to register
_classes = [
    hardnessOperator,
    convergeOperator,
    PGP_PT_sidebarPanel
]

def register():
    for cls in _classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in _classes:
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
pongbuster
8 Sep 2022

3 Comments

matt6

Hi, thanks for this. I've been using it for a while and decided to make some changes and an addition. Here are my changes:

  • Added a new operator "Set Stroke Properties" that will set hardness, pressure or strength of selected strokes/points to a given value.
  • All operators now respect layer visibility and locking status, so changing the hardness of a stroke won't also change the hardness for strokes that aren't visible or are locked.
  • All operators now respect the multiframe editing setting, so you can change the hardness of strokes on multiple frames at once if they are selected and multiframe is enabled.
  • Changed icons and adjusted some of the operator descriptions
  • Renamed operators to be more in line with blender's naming conventions. This is more personal preference, it doesn't really matter.

Visual Changes The code is Here

Just posting this here since I couldn't find a github. If you want, I can set one up. It's just one script, so it doesn't seem like it would be worth it, but it would probably make it easier for others to find it and contribute.

matt6

For others looking, pongbuster has a github with this and various other addons

modormoder

This is not the place for your additions. It will be inconvenient to use. Try to use github, where you can say it in 1 click