モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

gimp向けパースグリッドプラグイン、とりあえず修正〜

昨日思いついた機能を即実装!

f:id:dothiko:20141209121102j:plain

スケスケの枠ではなく、現在の背景色でグリッド内の背景を塗りつぶす機能を付けてみました。 あんまり意味が無い気もしなくもないですが(汗

思いついた機能は実装して使ってみたくなるのが人の常であります。gimpはこういうことが手軽に出来るのが実によいですな。kritaはそんなプラグインシステム、ないし…mypaintはUI部分がpythonなので実に楽しく弄れるのですけども。

あとC#の移植時のコメント化したコード断片やら何やらの見苦しい部分、あと実はプラグイン関数の引数部分が正常に動作してない感じでしたのでそこも修正しました。

今日のアフィは「リアルなキャラクターを描くためのデッサン講座」でございます。と、何やら料理風にさらっとアフィリンクを流すテスツw

さて、修正版です。

perspective_grid.py

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#[license] GPLv3
#[plugin]
#[name] perspective-grid-layer
#[desc] 
#透視法に基づくグリッドの描かれたレイヤを選択されたパスに基づいて生成
#[version]
#0.1 初期リリース
#[end]

#  このプログラムはGPLライセンスver3で公開します。
# 
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You may have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.           
#
#   Copyright 2014 dothiko(http://dothiko.hatenablog.com/) 


from gimpfu import *
from vector2d import *
import random


class Perspective_grid:

    def __init__(self,image,thick_size,mid_size,thin_size):
        self.BD=vector2d()
        self.AB=vector2d()
        self.BC=vector2d()
        self.AC=vector2d()
        self.img=image
        self.id=random.random() * 2147483647
        self.sizes=(thick_size,mid_size,thin_size)
        

    def main(self,raw_stroke,recursive_max,fill_bg):
        margin=32


        # the gridpt contains 4 lists,they are list of vectors to represent grid startpoints
        # that index should be...
        #  
        #  A---0---B
        #  |       |
        #  3   c   1
        #  |       |
        #  D---2---C
        #  
        #  the each number of ridges is the index of grid startpoint list(self.gridpt).

        self.m_gridpt=([],[],[],[])
        
        self.m_basevectors=[]
        for i in range(0,4):
            cv=vector2d()
            cv.x=raw_stroke[i*6+2]
            cv.y=raw_stroke[i*6+3]
            self.m_basevectors.append(cv)

        # culculate min-max coordinate of assigned vectors
        # and use them for layer dimension.
        min_x=min(self.m_basevectors,key=lambda v:v.x).x
        min_y=min(self.m_basevectors,key=lambda v:v.y).y
        max_x=max(self.m_basevectors,key=lambda v:v.x).x
        max_y=max(self.m_basevectors,key=lambda v:v.y).y

        width=int(max_x - min_x)+margin*2
        height=int(max_y - min_y)+margin*2
        top=int(min_y-margin)
        left=int(min_x-margin)

        # setup vanising points
        self.m_vp=(vector2d(),vector2d())

        self.get_crossed_point(self.m_vp[0],
                           self.m_basevectors[1],
                           self.m_basevectors[0],
                           self.m_basevectors[2],
                           self.m_basevectors[3]);

        self.get_crossed_point(self.m_vp[1],
                          self.m_basevectors[2],
                          self.m_basevectors[0],
                          self.m_basevectors[3],
                          self.m_basevectors[1]);


        self.m_recursive_max=recursive_max
        self.divide_plane()


        layer = gimp.Layer(self.img, "perspective grid layer",
                width, height, RGBA_IMAGE, 100, NORMAL_MODE)
        self.img.add_layer(layer,0)
        
        layer.set_offsets(left,top)
        self.draw_grid(layer,fill_bg)

    def get_crossed_point(self,ov,a,b,c,d):
        """
        get crossed point from vectors.

        Arguments:
        ov -- output vector, to consider recycle and low memory usage.
        a,b,c,d -- the vectors

        Returns:
        the cross-product of vectors,which indicates whether the crossed point actually crossed 
        when that value is positive.when negative,they are not crossed but to be crossed.
        """
        vector2d.sub(self.BD,d,b);
        vector2d.sub(self.AB,b,a);
        vector2d.sub(self.BC,c,b);
        vector2d.sub(self.AC,c,a);
        cross_AB=vector2d.cross(self.BD,self.AB);
        cross_BC=vector2d.cross(self.BD,self.BC);
        try:
            ratio=cross_AB / (cross_AB + cross_BC);
        except ZeroDivisionError:
            # vanising point dose not exist 
            ov.x=None
            ov.y=None
            return None

        ov.x=a.x + ratio*(self.AC.x);
        ov.y=a.y + ratio*(self.AC.y);

        return ratio;


    def divide_plane(self):
        self.divide_recursive(self.m_vp[0],
                         self.m_gridpt[0],self.m_gridpt[2],
                         self.m_basevectors[0],self.m_basevectors[1],
                         self.m_basevectors[2],self.m_basevectors[3],0);

        self.divide_recursive(self.m_vp[1],
                         self.m_gridpt[1],self.m_gridpt[3],
                         self.m_basevectors[1],self.m_basevectors[2],
                         self.m_basevectors[3],self.m_basevectors[0],0);


        # sort all points from base-vector
        tv=vector2d()

        def sort_key_A(cv):
            # get distance from corner "A"
            vector2d.sub(tv,cv,self.m_basevectors[0])
            return tv.get_scalar()

        def sort_key_D(cv):
            # get distance from corner "D"
            vector2d.sub(tv,cv,self.m_basevectors[3])
            return tv.get_scalar()

        def sort_key_B(cv):
            # get distance from corner "B"
            vector2d.sub(tv,cv,self.m_basevectors[1])
            return tv.get_scalar()

        self.m_gridpt[0].sort(key=sort_key_A)
        self.m_gridpt[1].sort(key=sort_key_B)
        self.m_gridpt[2].sort(key=sort_key_D)
        self.m_gridpt[3].sort(key=sort_key_A)

    def divide_recursive(self,vp,gridpt1,gridpt2,a,b,c,d,depth):
        if(depth > self.m_recursive_max):
            return;

        self.divide_single_plane(vp,
                            gridpt1,gridpt2,
                            a,b,
                            c,d);

        hab=gridpt1[-1];
        hcd=gridpt2[-1];

        self.divide_recursive(vp,gridpt1,gridpt2,
                         a,hab,
                         hcd,d,depth+1);

        self.divide_recursive(vp,gridpt1,gridpt2,
                         hab,b,
                         c,hcd,depth+1);

    def divide_line(self,array,targetpt,vp,a,b):
        curpt=vector2d();
        self.get_crossed_point(curpt,
                          vp,
                          a,
                          targetpt,
                          b);

        array.append(curpt); 
        return curpt;

    def divide_single_plane(self,vp,array1,array2,a,b,c,d):
        center=vector2d();
        self.get_crossed_point(center,
                          a,
                          b,
                          c,
                          d);

        if vp.x==None:
            # this means...vanising point does not exist!!(the target ridges are palallel!)
            nvp=a-d
            nvp.normalize_self()
            nvp+=center
            self.divide_line(array1,center,nvp,a,b);
            self.divide_line(array2,center,nvp,c,d);
        else:
            self.divide_line(array1,center,vp,a,b);
            self.divide_line(array2,center,vp,c,d);
        return center

    @staticmethod
    def generate_stroke_list(srcarray):
        """
        generate gimp-stroke array from srcarray.

        Arguments:
        srcarray -- the array of vector

        Returns:
        generated list
        """

        retlst=[]
        for cv in srcarray:
            i=0
            while i<3:
                retlst.append(cv.x)
                retlst.append(cv.y)
                i+=1

        return retlst

    @staticmethod
    def get_control_point_count(stroke):
        return len(stroke)/6


    def draw_grid(self,layer,fill_bg):


        pathes=[]

        def generate_pathobj(dst_size,force):
            if self.sizes[0]==dst_size and force==False:
                return pathes[0]
            else:
                cp=pdb.gimp_vectors_new(self.img,"pg_path_%d_%08x" % (dst_size,self.id))
                pdb.gimp_image_insert_vectors(self.img,cp,None,0)
                return cp


        for i in range(0,3):
            pathes.append(generate_pathobj(self.sizes[i],i==0))


        # setup grid base rectangle
        ts=Perspective_grid.generate_stroke_list(self.m_basevectors)


        # setup grid lines
        def append_CAC_point(ss,cv):
            for r in range(0,3):# to satisfy 'CAC' control point format
                ss.append(cv.x)
                ss.append(cv.y)

        # the original rectangle
        ss=[]
        for i in range(0,4):# m_basevectors index
            append_CAC_point(ss,self.m_basevectors[i])

        sid=pdb.gimp_vectors_stroke_new_from_points(pathes[0],0,
                len(ss),ss,True)

        if fill_bg:
            cursel=pdb.gimp_selection_save(self.img)
            pdb.gimp_image_select_item(self.img,2,pathes[0]) # replace
            pdb.gimp_edit_fill(layer,1) # bg fill
            pdb.gimp_image_select_item(self.img,2,cursel)

        # inner grid
        for i in range(0,2):# m_gridpt index
            for t in range(0,len(self.m_gridpt[i])):
                ss=[]
                append_CAC_point(ss,self.m_gridpt[i][t])
                append_CAC_point(ss,self.m_gridpt[i+2][t])

                if t != int(len(self.m_gridpt[i]) / 2):
                    sid=pdb.gimp_vectors_stroke_new_from_points(pathes[2],0,
                            len(ss),ss,0)
                else:
                    sid=pdb.gimp_vectors_stroke_new_from_points(pathes[1],0,
                            len(ss),ss,0)
        


        # save current contexts        
        pdb.gimp_context_push()

        # now,draw them.
        pdb.gimp_context_set_paint_method("gimp-paintbrush")
        bname="brush_%08x" % self.id
        pdb.gimp_brush_new(bname)
        pdb.gimp_brush_set_shape(bname,0)# 0 Round brush 1=square brush
        pdb.gimp_context_set_brush(bname)
        pdb.gimp_context_set_dynamics("Dynamics Off")
        pdb.gimp_brush_set_radius(bname,self.sizes[0]*2) # i dont know really how it works...
        pdb.gimp_brush_set_hardness(bname,0.7)# blurness
        pdb.gimp_brush_set_spacing(bname,5)

        def draw_single_path(dpath):
            if len(dpath.strokes)==0 or len(dpath.strokes[0].points[0])==0:
                    pass
            else:
                pdb.gimp_edit_stroke_vectors(layer,dpath)

            # deleting path
            pdb.gimp_image_remove_vectors(self.img,dpath)

        for i in range(0,3):
            pdb.gimp_context_set_brush_size(self.sizes[i])
            draw_single_path(pathes[i])

        pdb.gimp_brush_delete(bname)

        # do not forget to pop context.
        pdb.gimp_context_pop()

    def draw_stroke(self,stroke):
        pass

def python_fu_perspective_grid_func(a_img,a_drawable,divcount=2,delete_srcpath=False,thickness=4,same_thickness=False,fill_bg=False):

    # check the current active vector is suitable for later operations.
    v=pdb.gimp_image_get_active_vectors(a_img)
    if v==None:
       #pdb.gimp_message("please select a rectangular path")
        pdb.gimp_message("矩形パスを選択してください。")
        return

    if len(v.strokes)!=1:
       #pdb.gimp_message("the stroke should be a rectangle,and only one at path.")
        pdb.gimp_message("このスクリプトは、ひとつの矩形だけを持つパスにしか対応しません")
        return

    ctl_cnt=len(v.strokes[0].points[0])/6

    if ctl_cnt!=4:
       #pdb.gimp_message("the stroke should be a rectangle.this stroke of path has %d control points." % ctl_cnt)
        pdb.gimp_message("このパスは %d つの制御点を持ち、矩形ではありません。" % ctl_cnt)
        return



    if same_thickness:
        thick_size=mid_size=thin_size=thickness
    else:
        thick_size=thickness
        mid_size=thick_size*0.75
        thin_size=mid_size*0.5

    # start of groping undoable operations
    try:
        pdb.gimp_image_undo_group_start(a_img)

        p=Perspective_grid(a_img,thick_size,mid_size,thin_size)
        p.main(v.strokes[0].points[0],divcount,fill_bg)

        if delete_srcpath:
            pdb.gimp_image_remove_vectors(a_img,v)


    except Exception,e:
        print(str(e))
        import traceback
        print(traceback.format_exc())
    # end of grouping undoable operations
    finally:
        pdb.gimp_image_undo_group_end(a_img)





register(
        "python_fu_perspective_grid_func",
        "perspective-grid-layer",
        "creating perspective grid layer from a rectangular-formed path.",
        "dothiko",
        "kakukaku world",
        "dec 2014", 
        "<Image>/Python-Fu/layer/perspective-grid-layer", 
        "RGB*,GRAY*",
        [
            (PF_OPTION,"divcount","分割数",1,("4分割(2x2)","16分割(4x4)","64分割(8x8)","256分割(16x16)")),
            (PF_BOOL,"delete_srcpath","対象パスを消去する",True),
            (PF_ADJUSTMENT,"thickness","描線の太さ",4,(1,8,1)),
            (PF_BOOL,"same_thickness","全て同じ太さにする",False),
            (PF_BOOL,"fill_bg","背景色で塗りつぶす",False),
        ],
        [],
        python_fu_perspective_grid_func)


main()