Source code for board.cboard

# coding: utf_8
# 画盤モジュール
# cboard.py
"""画盤モジュール

* 画盤システムの実装モジュール.
* 220815: Created by Hiroki Arimura, arim@ist.hokudai.ac.jp 
* based on board.py 

Attributes: 

DEFAULT_PPT = 720 (int): デフォールトの解像度
DEFAULT_LINE_WIDTH = 1 (float): デフォールトの辺幅
DEFAULT_COLOR_BORDER_INNER = 'red' (str): デフォールトの境界色(内側)
DEFAULT_COLOR_BORDER_OUTER = 'blue' (str): デフォールトの境界色(外側)
"""
import sys
import math 
import numpy as np
import random 
import copy 
from typing import NamedTuple
# import inspect
# import cairo 

import os #for rtd
sys.path.insert(0, os.path.abspath('../board/')) #for rtd

import common as com
import kwargs as kw
import crtool as crt
import loggable as log 
import backupcaller as bc

# DEFAULT_PPI = 720 
# DEFAULT_LINE_WIDTH = 1 
# DEFAULT_COLOR_BORDER_INNER = 'red'
# DEFAULT_COLOR_BORDER_OUTER = 'blue'
# EMPTY_RECT = ((0,0), (0,0))

## PARAMS 2022 version
EMPTY_RECT = (0.0, 0.0, 0.0, 0.0)
MYCOLS = list(crt.DARKCOL.values())
#MYCOLS = list(crt.MYCOL.values())
DEFAULT_LINE_WIDTH=1

# 辞書(語彙)


# #constants
# CMD_MOVE = 0
# CMD_LINE = 1

[docs]def numpair_normalize(margin=None, default=None): """マージン指定を正規化する.margin が数の対(float,float)ならばそのまま返し, margin が数ならば,(margin,margin)を返す. margin==Noneのときは,(0.0,0.0)を返す. """ if margin==None: margin1 = com.ensure_defined(value=default, default=(0.0, 0.0)) elif isinstance(margin, (float,int)): margin1 = (margin, margin) elif com.is_typeof_seq(margin, etype=(float,int), dim=2): margin1 = margin else: com.panic(f'margin={margin} must be either float or (float,float)!') com.ensure_point(margin1, name='margin1') return margin1
[docs]def perturb_point(x=0.0, y=0.0, max_perturb=0.0): """与えられた点 (x, y) に対して,幅 [(-1)*max_perturb, (+1)*max_perturb] の摂動を与えた点(x1, y1)を返す. Args: x, y (float) : 入力点のx- and y-coordinates.デフォールト値 x = y = 0.0 Returns: x1, y1 (float) : 摂動を加えた点のx- and y-coordinates Notes: xとyが省略されたときは,値0.0, 0.0を中心とした摂動を返す. """ if max_perturb==0.0: print(f'warning: cboard.perturb_point: max_perturb==0.0!') x1 = x + max_perturb*random.random() y1 = y + max_perturb*random.random() return x1, y1
[docs]def pair_normalize(pair=None): """変換と子ボードの対を分解し,検査して返す. Args: pair (tuple()) : Returns: (tuple()) : 変換と子ボードの対 (trans, child) """ trans, child = pair #分解 #子の型チェック if child != None: com.ensure(isinstance(child, BoardBase), 'child must be a subclass of BoardBase!: {child}') if trans!=None: com.ensure(isinstance(trans, crt.GeoTransform), f'trans must be of crt.GeoTransform: {type(trans)}') return trans, child
#===== # 画盤(board)のクラス: 座標系オブジェクト #===== # class Board(com.Loggable):
[docs]class BoardBase(log.Loggable): """図形(Board)の描画に関する基本機能を提供するクラス.子孫クラスから呼び出される二つの私的関数`_arrange()`と`_draw(self, cr)`を提供する. Args: anchor_str (str, tuple(str,str)) : 文字列対によるアンカー表記.それ自体が`None`でも良いし,対の片方または両方の要素が`None`を取っても良い. shape (tuple(float,float)) : 形状ベクトル `(sx, sy) in R^2` **kwargs : 他のキーワード引数.上位クラスに渡される. Attributes: trans (GeoTransform) : 自身の変換 box (tuple(float,float,float,float)) : 自身の包含矩形. boxes (float,float,float,float) : 子全体の包含矩形. anchor (tuple(float,float)) : 正規化アンカーベクトル `a = (ax,ay) in [0,1]^2`.配置時点での包含矩形に対する原点の相対位置を表す. verbose (bool): ログ出力のフラグ Notes: 本クラスでは,主ループとして,配置関数`_arrange()`と描画関数`_draw(self, cr)`を提供し,子孫クラスにおいて,これらの以下のサブメソッドを実装することで,独自の振る舞いを定義する. * 関数`_arrange()`のサブメソッド * `arrange_box_children(self)`: 子を相互に配置し,子全体の包含矩形`self.boxes`を求める. * `arrange_box_self(self)`: 子全体の包含矩形`self.boxes`と修飾情報(modifiers)から自身の包含矩形を求める.ボードは,修飾情報として,余白やアンカー点の情報をもつ. * 関数`_draw()`のサブメソッド: * `draw_me_before(self, cr)`: Cairoの文脈オブジェクト`cr`を受け取り,子の描画の前に,自身を描画する手続きを定義する. * `draw_me_after(self, cr)`: Cairoの文脈オブジェクト`cr`を受け取り,子の描画の後に,自身を描画する手続きを定義する. Note: アンカー表記の対の要素に`None`を許す. * アンカー表記自身が`None`のときは,値としてDEFAULT_ANCHOR_STRをとる.現在は,`ANCHOR_STR_ORIGIN = ('left','top')`である. * 対の要素が`None`のときは,値`None`を,`mid`に相当する値`DEFAULT_ANCHOR_VALUE = 0.5`に置き換える. Example:: anchor=('left' , 'top') => (0.0, 0.0) anchor=('mid' , 'mid') => (0.5, 0.5) anchor=('right', 'bot') => (1.0, 1.0) anchor=('left' , None) => (0.0, 0.5) anchor=(None , 'top') => (0.5, 0.0) """ def __init__(self, shape=None, anchor=None, **kwargs): """画盤オブジェクトを初期化する """ #引数 #親Loggableの初期化 super().__init__(**kwargs) #内部変数 self.anchor = crt.anchor_vector(anchor_str=anchor, strict=True) # self.anchor = self.create_anchor_vector(anchor_str=anchor) self.shape : tuple = None self.set_shape(shape=shape, is_nullable=True) #self.shape ##自身の配置情報 self.trans : crt.GeoTransform = None #自身の変換 self.box = None #自身の包含矩形. self.boxes = None #子全体の包含矩形. if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return #===== # 雑関数 #=====
[docs] def get_box(self) -> 'tuple(float,float,float,float)': """保持する自身の包含矩形を返す. Returns: (tuple(float,float,float,float)) : 自身の包含矩形.親による自身の配置を規定する. """ # _box = self.box com.ensure_box(self.box, name='self.box', nullable=False) return self.box
[docs] def get_trans(self) -> crt.GeoTransform: """保持する変換を返す. Returns: (GeoTransform) : 自身に適用する変換.自身の子の配置を規定する. """ _trans = self.trans #self.transはNone(降等変換)でも良い. com.ensure(_trans==None or isinstance(_trans, crt.GeoTransform), f'get_trans: trans={_trans} must be a GeoTrans') return _trans
# def create_anchor_vector(self, anchor_str=None): # """アンカー文字列指定`anchor_str`を受け取り,アンカーベクトルを生成して返す.""" # #アンカーのデフォールト設定 # _anchor_vect = crt.anchor_vector(anchor_str=anchor_str) # com.ensure_point(_anchor_vect, name='_anchor_vect', nullable=False) # return _anchor_vect
[docs] def get_anchor(self) -> 'tuple(float,float)': """自身の包含矩形上のアンカー点(配置の原点)を返す.ここに,各種の配置関数`_arrange()`は,アンカー点を原点として図形を配置する. Returns: (tuple(float, float)) : 2次元平面上のアンカー点 `b = (bx,by) in R^2` Notes: 返り値は,次の属性に保持されている. * self.anchor (tuple(float,float)) : 正規化アンカーベクトル `a = (ax,ay) in [0,1]^2` """ com.ensure_point(self.anchor, name='self.anchor', nullable=False) apos = crt.anchor_point_by_vector(box=self.box, vect=self.anchor) com.ensure_point(apos, name='apos', nullable=False) return apos
[docs] def set_shape(self, shape=None, is_nullable=False): """矩形形状を設定する. Returns: (Board) : 自分自身を返す. """ self.shape = com.ensure_point(shape, name='shape', nullable=is_nullable) return self
# exp 基本:配置の計算 # def _arrange(self): def _arrange(self, shape=None): """配置を計算する.配置は,ボトムアップに再帰的に計算される. Returns: rect: 計算済みの自身の包含矩形オブジェクト """ if self.verbose: self.repo(msg=f'@debug0@{self.myinfo()}._arrange(shape={shape}): { self.vars() }') if self.shape==None: self.shape = shape #exp #子の配置情報を再帰的に得る self._arrange_apply_children(shape=shape) #子供全てを再配置する. self.arrange_box_children() #注意:関数arrange_self_transformはサブクラスでオーバーライドする self.arrange_box_self() if False: print(f'@debug:trans:{self.myinfo()}: trans={self.trans}') com.ensure(self.box != None, f'self.box={self.box} != None!') return self.box def _arrange_apply_children(self, shape=None): """自身の子すべてに対して,再帰的に配置を行う. 次の属性を操作する: * self.children_: 読み出し """ for idx, pair in self.children_enumerated(): trans, child = pair_normalize(pair) if self.verbose: self.repo(msg=f'=> call _arrange(shape={shape}) on {idx}-th child={child.myinfo()}', is_child=True, header=False) child._arrange(shape=shape) #再帰的に子の配置を実行 # child._arrange() #再帰的に子の配置を実行 com.ensure(child.get_box() != None, f'child.get_box()={child.get_box()} != None') return
[docs] def arrange_box_children(self): """自身の子たちの配置情報を計算する.子孫クラスでオーバーライドすること. Note: 配置情報として,子リストにおいて子それぞれの変換と子自身を計算する.終了前に属性`self.boxes`を設定すること. 次の属性を操作する: * 読み出し: `self.children_` * 書き込み: `self.boxes` (非None) """ _boxes = EMPTY_RECT for idx, pair in self.children_enumerated(): trans, child = pair_normalize(pair) #自身の包含矩形の更新 box = child.get_box() com.ensure(box != None, f'child.get_box()={box} != None') box = crt.box_apply_trans(box, trans=trans) _boxes = crt.box_union(_boxes, box) #包含矩形の更新 self.boxes = com.ensure_box(_boxes, name='_boxes', nullable=False) if self.verbose: self.repo(msg=f'{self.myinfo()}.arrange_box_children()=> box={ self.boxes }', is_child=True) return
#To be Override
[docs] def arrange_box_self(self): """修飾情報から自身の配置情報を計算する.子孫クラスでオーバーライドすること Notes: 自身の配置情報として変換と包含矩形を求める.終了前に属性`self.box`と`self.trans`を設定すること.次の属性を操作する: * 読み出し: `self.boxes` (非None) * 書き込み: `self.box` (非None) * 書き込み: `self.trans` (Noneも許す) """ self.box = self.boxes return
#===== # 描画 #===== def _draw(self, cr): """トップダウンに画像を描画する. Args: trans (crt.GeoTransform) : 空間変換 pm0 (pim.ImageBoard) : 基本描画のプリミティブボード """ if self.verbose: self.repo(is_child=True, msg=f'{self.myinfo()}._draw(): { self.vars() }') ## 自分の空間を開く cr.save() ##self ## 自身の変換を適用する crt.cr_apply_trans(trans=self.get_trans(), context=cr) if True: print(f'@debug5:draw:self:{self.myinfo()}: apply trans={self.get_trans()}') #必要なら自分の描画を行う self.draw_me_before(cr) #子の描画を行う for idx, pair in self.children_enumerated(): trans, child = pair #分解 #子の型チェック com.ensure(isinstance(child, BoardBase), f'{self.myinfo()}.draw: child must be a subclass of'+ f' BoardBase!: {child}: children_={self.children_}') cr.save() ## 子の空間を開く crt.cr_apply_trans(trans=trans, context=cr) ## 変換を適用する if False: print(f'- @debug:draw:child:{child.myinfo()}: apply trans={trans}') child._draw(cr) cr.restore() ## 子の空間を閉じる #必要なら自分の描画を行う self.draw_me_after(cr) ## 自分の空間を閉じる cr.restore() ##self ## debug: 包含矩形を描画する self.draw_origin_and_box(cr) #自分の原点位置と包含矩形を描画する. return # 派生:Override
[docs] def draw_me_before(self, cr): """自分の描画を行う.子の描画の前に実行される.子孫クラスでオーバーライドすること. Args: cr (Cairo.Context) : Cairoの文脈オブジェクト Notes: 文脈オブジェクトに,配置情報を元に直接書き込みを行う. """ if self.verbose: self.repo(is_child=True, msg=f'BoardBase:{self.myinfo()}.draw_me_before()... trans={self.trans} box={self.box} ') ## ここでcontext crに何か描く. return
# 派生:Override
[docs] def draw_me_after(self, cr): """自分の描画を行う.子の描画の前に実行される.子孫クラスでオーバーライドすること. Args: cr (Cairo.Context) : Cairoの文脈オブジェクト Notes: 文脈オブジェクトに,配置情報を元に直接書き込みを行う. """ if self.verbose: self.repo(is_child=True, msg=f'BoardBase:{self.myinfo()}.draw_me_before()... trans={self.trans} box={self.box} ') ## ここでcontext crに何か描く. return
#===== # おまけ関数.drawの副関数 #=====
[docs] def draw_origin_and_box(self, cr): """自分の原点位置と包含矩形をマーカーで描画する. """ #自分の包含矩形を描画する. if (self.fetch(key='boundingbox', default=False) and com.ensure_box(self.get_box(), name='get_box()', to_check_only=True)): self.draw_perturbed_box(self.get_box(), context=cr) #自分の原点位置をマーカーで描画する. if kw.get(self.kwargs, key='show_origin', default=False): rgb = kw.get(self.kwargs, key='rgb_origin', default=crt.MYCOL['red']) angle = kw.get(self.kwargs, key='angle_origin', default=math.pi/4) crt.cr_draw_marker_cross(context=cr, x=0,y=0, linewidth=0.5, angle=angle, rgb=crt.cr_add_alpha(rgb, alpha=0.5)) #自分の包含矩形を描画する. if kw.get(self.kwargs, key='show_box', default=False): rgb = kw.get(self.kwargs, key='rgb_box', default=crt.MYCOL['green']) (x0, y0, x1, y1) = self.get_box() crt.cr_rectangle(x0, y0, x1 - x0, y1 - y0, context=cr, rgb=crt.cr_add_alpha(rgb, alpha=0.5), line_width=1) return
[docs] def draw_perturbed_box(self, box, context=None, max_perturb=None): """自分の包含矩形を描画する.視認性のため, max_perturbで決まる摂動を加えて描画する. """ cr = context box = crt.box_normalize(self.get_box()) x, y = box[0], box[1] width, height=(box[2]-box[0]), (box[3]-box[1]) ## 色を選ぶ rgb = MYCOLS[0] ## 色 rgb_alpha = 0.25 ## 色の透明度 # rgb_alpha = 0.5 if self.depth: rgb = MYCOLS[self.depth % len(MYCOLS)] ## 色 rgb = (rgb[0], rgb[1], rgb[2], rgb_alpha) else: if self.verbose: print(f'debug: self.depth=None at { self.myinfo() }') ## 位置を微小変動させる max_perturb = self.fetch(key='max_perturb', default=8.0 * DEFAULT_LINE_WIDTH) com.ensure(max_perturb != None, f'max_perturb={max_perturb} must be defined!') dx = dy = max_perturb*random.random() - 0.5*max_perturb crt.cr_rectangle(x + dx, y + dy, width, height, source_rgb=rgb, line_width=1, context=cr) ## キャプション tx, ty = x + dx, y + dy fsize = 4 fmargin = 0.25*fsize msg = self.myinfo() x, y, width, height, dx, dy = crt.cr_text_extent(tx, ty, msg=msg, context=cr) crt.cr_text(tx + fmargin, ty + 1.0*dy + fmargin, msg=msg, fsize=fsize, context=cr) return
pass ## end class BoardBase
#===== # ボードのインタフェースのクラス #===== #old: class Board(AnchorBoard):
[docs]class CoreBoard(BoardBase): """描画を行う画盤(board)のクラス.具体的な子の配置方法は持たず,サブクラスで実装される.`WrapperBoard`とは比較不能(兄弟)な継承関係をもつ. Args: **kwargs : 他のキーワード引数.上位クラスに渡される. """ def __init__(self, **kwargs): """画盤オブジェクトを初期化する """ #引数 super().__init__(**kwargs) if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
#===== # ラッパーのクラス #=====
[docs]class WrapperBoard(BoardBase): """ラッパーのクラス.描画は行わず,唯一の子の位置パラメータ決めのみを行う.自身が持たないメソッド呼び出しを,唯一の子に転送する. `CoreBoard`とは比較不能(兄弟)な継承関係をもつ. Args: child (BoardBase) : 子として保持するBoardBaseオブジェクト. **kwargs : 他のキーワード引数.上位クラスに渡される. Attributes: verbose (bool): ログ出力のフラグ """ # def __init__(self, child=None, **kwargs): def __init__(self, child=None, **kwargs): """画盤オブジェクトを初期化する """ #引数 #親Loggableの初期化 super().__init__(max_children=1, #Can have at most one child **kwargs) ## 包み込む唯一の子を設定する com.ensure(child != None, f'child must be to None!') child.parent = None ##使用済みの子から親への参照を切る.破壊的代入. # self.put(trans=None, child=child) #子に追加 self.append(pair=(None, child)) #子に追加 ## メソッド転送設定 self.the_child : BoardBase = child #転送先オブジェクト #内部変数 if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
[docs] def get_the_child(self) -> BoardBase: _child = self.the_child com.ensure(isinstance(_child, BoardBase), 'child must be a subclass of BoardBase!: {_child}') return _child
#===== ## メソッド転送 #===== def __getattr__(self, name): """メソッドが未定義のとき,呼び出される特殊関数. 未定義の属性呼び出しのときも呼ばれるので注意. """ if self.verbose: print(f'\t@WrapperBoard:{self.myinfo()}.__getattr__() is called with method="{name}" ') return bc.BackupCaller(self.the_child, name, verbose=False) #===== # 子のリスト #===== def _arrange_apply_children(self, shape=None): """自身の子すべてに対して,再帰的に配置を行う.オーバーライド. 次の属性を操作する: * self.children_: 読み出し """ if self.verbose: print() if self.verbose: self.repo(msg=f'\t@debug1@WrapperBoard:{self.myinfo()}._arrange_apply_children(shape={shape}) is called with the_child={self.get_the_child().myinfo()} the_child.vars={self.get_the_child().vars()} self.vars={self.vars()}', is_child=True, header=False) child = self.the_child trans = self.trans #形状を設定する if self.shape==None: self.shape = shape #exp #self.shape = shape shape_child = self.box_propagate_downward(shape=shape) child._arrange(shape=shape_child) #再帰的に子の配置を実行 com.ensure_box(child.get_box(), name='child.get_box()', nullable=False) return # Override exp 基本:配置の計算
[docs] def arrange_box_self(self): """アンカー情報から,変換と包含矩形を計算する.子孫クラスでオーバーライドする. 次の属性を操作する: * self.boxes : 読み出し * self.box : 書き込み * self.trans : 書き込み crt.ANCHOR_ORIGINは,左上原点のアンカー指定(left,top) """ child_box = self.get_the_child().get_box() com.ensure_box(child_box, name='child_box', nullable=False) box, move = self.box_propagate_upward(child_box=child_box) #自身の包含矩形とアンカーを,左上を原点にそろえて,正規化する. self.trans : crt.GeoTransform = crt.Translate(dest=move) self.box : tuple = crt.box_apply_trans(box, trans=self.trans) if self.verbose: self.repo(msg=f'\t@debug2@WrapperBoard:{self.myinfo()}._arrange_box_self() returned with trans={self.trans} box="{self.box}" the_child={self.get_the_child().myinfo()} the_child.vars={self.get_the_child().vars()} self.vars={self.vars()}', is_child=True, header=False) return self.box
[docs] def box_propagate_downward(self, shape=None): """親から矩形形状`shape=(sx,sy)`を受け取り,子の矩形形状を返す. 子孫クラスでオーバライドして用いる. """ return shape
[docs] def box_propagate_upward(self, child_box=None): """子矩形`box`を受け取り,親矩形と並行移動ベクトルを返す. 子孫クラスでオーバライドして用いる. """ move = (0.0, 0.0) #identity return child_box, move
pass #class WrapperBoard
#===== # ラッパーのサブクラス #=====
[docs]class MarginWrapper(WrapperBoard): """唯一の子を指定した余白(margin)で包むラッパーのクラス, 自身が持たないメソッド呼び出しを子に転送する. Args: child (BoardBase) : 子として保持するBoardBaseオブジェクト. margin (float, tuple(float, float)) : 子の外周に付与する余白の情報 **kwargs : 他のキーワード引数.上位クラスに渡される. Attributes: margin (tuple(float,float)) : 余白情報 margin = (margin_x, margin_y). box (tuple(float,float,float,float)) : 修正された包含矩形.子の包含矩形の外側にmarginで指定したx方向とy方向の幅の余白を拡大した形状になる.関数`get_box()`で返される. trans (GeoTransform) : 修正された変換.拡大された包含矩形の左上隅を原点とする. verbose (bool): ログ出力のフラグ """ # def __init__(self, child=None, **kwargs): def __init__(self, child=None, margin=None, **kwargs): """画盤オブジェクトを初期化する """ #引数 #親Loggableの初期化 super().__init__(child=child, **kwargs) ##マージン self.margin : tuple = margin #内部変数 if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
[docs] def box_propagate_downward(self, shape=None): #変換を求める if shape==None: return shape else: mx, my = numpair_normalize(margin=self.margin) x0, y0 = shape shape0 = x0 - 2*mx, y0 - 2*my return shape0
[docs] def box_propagate_upward(self, child_box=None): #マージンを求める mx, my = numpair_normalize(margin=self.margin) #子のアンカー点を求める x0, y0, x1, y1 = child_box dst = 0.0, 0.0 #親のアンカー点を求める self_box = x0 - mx, y0 - my, x1 + mx, y1 + my xx0, yy0, xx1, yy1 = self_box src = xx0, yy0 #変換を求める move = crt.vt_sub(dst, src) #move = dst - src return self_box, move
pass #class MarginWrapper
#===== # ラッパーのサブクラス #===== #old: class Board(AnchorBoard):
[docs]class SideWrapper(WrapperBoard): """唯一の子を,指定した側面(`side`)に基づいて配置するラッパーのクラス. 自身が持たないメソッド呼び出しを子に転送する. Args: **kwargs : 他のキーワード引数.上位クラスに渡される. """ def __init__(self, side=None, **kwargs): """画盤オブジェクトを初期化する """ #引数 super().__init__(**kwargs) #内部変数 self.side = crt.anchor_vector(anchor_str=side, strict=True) if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
[docs] def box_propagate_downward(self, shape=None): if True: print(f'@debug7:SideWrapper[{self.the_child.myinfo()}].box_propagate_downward(): shape={shape} self.shape={self.shape}; self={self.myinfo()} self.box={self.box}') #変換を求める.そのまま返す. if self.shape==None: self.shape = shape #exp #self.shape = shape return shape
[docs] def box_propagate_upward(self, child_box=None): #アンカーベクトルを得る com.ensure_point(self.side, name='self.side', nullable=False) #子のアンカー点を求める src = crt.anchor_point_by_vector(box=child_box, vect=self.side) com.ensure_point(src, name='src', nullable=False) #親のアンカー点を求める if self.shape==None: self_box = child_box dst = src move = crt.vt_sub(src, src) else: #アンカー点を求める self_box = crt.box_from_shape(shape=self.shape) #形状から求める dst = crt.anchor_point_by_vector(box=self_box, vect=self.side) com.ensure_point(dst, name='dest', nullable=False) #変換を求める move = crt.vt_sub(dst, src) #move = dst - src #デバッグ if self.verbose: self.repo(msg=f'\t@debug3@prop@WrapperBoard:{self.myinfo()}.box_propagate_upward() is called: shape={self.shape}\n'+ f'@debug3:child={self.get_the_child().myinfo()}: src={src} box={self.get_the_child().box}\n'+ f'@debug3:self={self.myinfo()}: dst={dst} box={self_box}', is_child=True, header=False) # self.box = self_box if True: print(f'@debug6:SideWrapper:{self.the_child.myinfo()}: self.shape={self.shape} src={src} dst={dst}; self={self.myinfo()}: obtain move={move} box{self_box}') return self_box, move
pass
#===== # ボードの実装クラス #===== ## TODO:PlaceBoard
[docs]class PlaceBoard(CoreBoard): """画盤(board)のクラス: 座標系オブジェクト Args: **kwargs : 他のキーワード引数.上位クラスに渡される. Attributes: verbose (bool): ログ出力のフラグ """ count = 0 #board id def __init__(self, **kwargs): """画盤オブジェクトを初期化する """ #引数 #親Loggableの初期化 super().__init__(**kwargs) #内部変数 if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return # exp 基本:子を追加する
[docs] def put(self, child=None, trans=None) -> BoardBase: """子を追加する. Args: trans (cairo.Matrix) : 自座標における子の配置を指示する変換行列.原点にある子を所望の場所に配置するためのアフィン変換を表す. child (Board): 子として追加するBoardオブジェクト Returns: (Board) : 追加した子 Example:: root = Canvas() child = root.put(Board()) child = parent.put(trans=Translate(dest=(1,2)), Rectangle()) """ #型チェック: 変換transは,Noneを許す. com.ensure(trans == None or isinstance(trans, crt.GeoTransform), f'{self.myinfo()}.put(): trans must be a GeoTransform!: {trans}') #型チェック: 子childはNoneを許さない. com.ensure(child != None and isinstance(child, BoardBase), f'{self.myinfo()}.put(): child must be a BoardBase!: {child}') self.append((trans, child)) #子リスト if self.verbose: self.repo(msg=f'=> added: {self.myinfo()}.put(): trans={trans} child={ child } with vars={ child.vars() }...') return child #Do not change!
#===== # ボードのインタフェースのクラス #===== #old: class Board(AnchorBoard):
[docs]class Board(PlaceBoard): """描画を行う画盤(board)のクラス.`PackerBoard`のラッパー. Args: **kwargs : 他のキーワード引数.上位クラスに渡される. """ def __init__(self, **kwargs): """画盤オブジェクトを初期化する """ #引数 super().__init__(**kwargs) if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
#====== #基盤画像系: exp #====== # class Canvas(Board):
[docs]class Canvas(PlaceBoard): """トップレベルのボードのクラス.描画のためのCairoのSurfaceを保持する. 描画対象のすべての要素(ボード)は,本オブジェクトの子孫として保持する. Args: imgtype (cairo.Format) : Surfaceフォーマット (default: cairo.FORMAT_ARGB32) format (str) : 出力ファイルフォーマット(拡張子 pdf, png)default: "pdf" outfile (str) : 出力ファイル名(拡張子を除く)default: "out" imagesize (str) : 初期の画像サイズ. default: 'XGA' verbose (bool): ログ出力のフラグ.default=False portrait (bool) : 画像サイズが縦長か? boundingbox (bool) : デバッグ用: 包含矩形のデバッグ出力をする max_perturb (float) : デバッグ用: 包含矩形の摂動幅 """ # background (string): 背景色.デバッグ用.default='skyblue', #debug def __init__(self, imgtype=crt.DEFAULT_IMGTYPE, #cairoのSurface format # imgtype=cairo.FORMAT_ARGB32, #cairoのSurface format format="pdf", #出力ファイルフォーマット(拡張子 pdf, png) outfile="out", #出力ファイル名(拡張子を除く) imagesize=None, #初期の画像サイズ # imagesize='XGA',#初期の画像サイズ portrait=False, #画像サイズが縦長か? ## for debugging boundingbox=False, #デバッグ用: 包含矩形のデバッグ出力をする max_perturb=None, #デバッグ用: **kwargs): """トップレベルのボードを生成する.描画のためのCairoのSurfaceを保持する. """ #基礎クラスの生成子 super().__init__(dep_init=0, **kwargs) #パラメータ self.imgtype = imgtype self.format = format self.outfile = outfile self.imagesize = imagesize self.portrait = portrait self.boundingbox = boundingbox #fetchで子孫からアクセス self.max_perturb : float = max_perturb #fetchで子孫からアクセス # self.show_origin = show_origin #fetchで子孫からアクセス verbose = kw.get(kwargs, key='verbose', default=False) com.ensure(self.format, f'format must be defined!') ##属性 self.im = None #基礎画像オブジェクト.遅延生成 # self.display_shape = None #画像サイズが決まっていれば,画像オブジェクトを生成する. #そうでなければ,配置完了後にshow()中で計算する. if self.imagesize: if False: print(f'@debug:create_image_object: {self.myinfo(depth=True)}.__init__()') ##画像サイズの設定 _display_shape = crt.get_display_shape(shape=self.imagesize, portrait=self.portrait) if _display_shape: self.im = self.create_image_object(_display_shape) if verbose: self.repo(msg=f'{self.myinfo()}.__init__(): vars={ self.vars() }') return
[docs] def create_image_object(self, display_shape=None): com.ensure(format, f'format must be defined!') com.ensure(display_shape!=None, f'display_shape={display_shape} must be defined!') ##ImageBoardの生成 if self.verbose: self.repo(msg=f'.create_pim: format={ format }') im = crt.ImageBoard( format=self.format, outfile=self.outfile, display_shape=display_shape, verbose=self.verbose, ) im.depth=self.depth+1 return im
#===== # 基底描画系の生成 #=====
[docs] def canvas_size(self) -> 'tuple(int, int)': return self.display_shape
# 基本:画像を表示する
[docs] def show(self, noshow=False, depth=0): """配置と描画を行ない,画像をディスプレイに表示する. 次の手順で描画する. - ステップ1: 再帰的に子オブジェクトへ _arrange() 命令を送り,ボトムアップに配置の包含矩形 box を計算する - ステップ2: 包含矩形情報 box を元に,self.create_pim() 命令を発行して,pillowの画像盤 self.im を生成する. - ステップ3: 再帰的に draw() 命令を発行して,トップダウンに描画を行う - ステップ4: 自身のもつpillowオブジェクト self.imに show() 命令を送り,画像を表示する """ if self.verbose: self.repo(msg=f'{self.myinfo()}.show(): { self.vars() }') #ステップ1: ボトムアップに配置を計算する box = self._arrange(shape=None) com.ensure(box, 'box={box} must be defined!') #ステップ1.5: 画像オブジェクトを生成する. if self.im==None: if False: print(f'@debug:create_image_object: {self.myinfo(depth=True)}.show()') self.im = self.create_image_object(display_shape=crt.box_to_shape(box)) #ステップ2: トップダウンに描画を行う cr = self.im.context() self._draw(cr) #ステップ3: 画像を表示する self.im.show(noshow=noshow) ##pilimage.ImageBoard return
#Cairo.contextオブジェクトの返却
[docs] def context(self) -> 'cairo.Context': """Cairo.contextオブジェクトを返す.""" return self.im.context() ##Cairo.contextオブジェクト
# 画像をファイルに保存する
[docs] def save(self, imgfile, **kwargs): """imgfile: 保存する画像ファイルの名前.""" self.im.save(imgfile=imgfile, **kwargs) return
#===== # ボードの実装クラス #===== # class PackerBoard(BoardBase):
[docs]class PackerBoard(CoreBoard): """画盤(board)のクラス.Boardのサブクラス Args: orient (str) : 並べる主軸方向の指定.`x`または`y`の値をとる.default='x' packing (str) : 内部のボードの詰め方の指定情報. packing in ('even','pack') width (float) : 自身の幅.default=None. height (float) : 自身の高さ.default=None. kwargs : 他のキーワード引数.上位クラスに渡される. """ def __init__(self, orient='x', packing=None, cell_margin=None, #exp cell_side=None, #exp ## # width=None, # height=None, **kwargs): #引数 super().__init__(**kwargs) #内部変数 ## マージン設定 self.packing : str = packing self.cell_margin : tuple = cell_margin self.cell_side : tuple = cell_side #内部変数 self.orient : str = orient #debug if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return # exp 基本:子を追加する
[docs] def add(self, child:BoardBase=None) -> BoardBase: """子を追加する.関数`BoardBase.put(child, trans=None)`へのラッパー関数. Args: child (Board): 子として追加するBoardオブジェクト Returns: (Board) : 追加した子 Example:: root = Canvas() parent= root.put(Board()) child = parent.add(Rectangle()) child = parent.add(Circle()) """ com.ensure(child != None, f'child must be to None!') if False: print(f'@debug: self={self.myinfo(depth=True)} child={child.myinfo(depth=True)}') #exp: ラッパーで包んでから,子リストに加える _debug_wrapper = {} if self.fetch(key='verbose', default=False): _debug_wrapper = { #デバッグ用の包含矩形描画 'show_box': True, # rgb_box=crt.MYCOL['magenta'], 'rgb_box': crt.cr_add_alpha(crt.MYCOL['magenta'], alpha=0.5), #デバッグ用の原点描画 'show_origin': True, 'angle_origin': (math.pi/4)*0, } # child1 = MarginWrapper(child=child, # margin=self.cell_margin, # **_debug_wrapper, #デバッグ表示用 # ) # return self.append(pair=(None, child1)) #exp child1 = SideWrapper(child=child, side=self.cell_side, rgb_origin=crt.MYCOL['green'], verbose=True, **_debug_wrapper, #デバッグ表示用 ) child2 = MarginWrapper(child=child1, margin=self.cell_margin, rgb_origin=crt.MYCOL['blue'], **_debug_wrapper, #デバッグ表示用 ) return self.append(pair=(None, child2)) #exp
# 基本:子画盤の並びを返す ##Override
[docs] def children_box_enumerated(self) -> 'iterator': """添字とエントリの対`idx, triple`の並び children を返す. 現在は,`triple`は,三つ組`trans, child, child_box`である.必要なら部分クラスで上書きする. Returns: (list): 三つ組`trans (GeoTransform)`, `child (BoardBase)`, child_box (tuple(float,float,float,float))`のリスト. Notes: ただし,childはNoneでないことを保証し,そうでないときはErrorを投げる. transとchild_boxはNoneでも良い. """ idx = 0 for trans, child in self.children_: #子の型チェック com.ensure(child != None and isinstance(child, BoardBase), 'child must be a subclass of BoardBase!: {child}') child_box = child.get_box() triple = trans, child, child_box yield idx, triple idx += 1
def _accumulate_boxes(boxes): """包含矩形のリストを受け取り,要素である包含矩形を走査して, サイズの最大と総和を求める副関数 Args: boxes (list(Box)) : 包含矩形の並び(リスト or iterator) Returns: max_shape (tuple(float, float)) : 矩形のx-とy-サイズの最大値の対 SUMSZ (tuple(float, float)) : 矩形x-とy-のサイズの総和の対 NUM (int) : 矩形の数 """ max_shape = [0, 0] #サイズの最大値 sum_shape = [0, 0] #サイズの総和 num_shape = 0 # for idx, triple in self.children_box_enumerated(): for idx, triple in boxes: trans, child, child_box = triple #分解 com.ensure(child_box != None, f'child_box={child_box} must be non-None') #子の包含矩形の最大サイズを更新 cwidth0, cwidth1 = crt.box_to_shape(child_box) max_shape[0], max_shape[1] = max(max_shape[0],cwidth0), max(max_shape[1],cwidth1) sum_shape[0], sum_shape[1] = sum_shape[0] + cwidth0, sum_shape[1] + cwidth1 num_shape += 1 return max_shape, sum_shape, num_shape def _get_axes(orient=None): """与えられた文字列orient (str)に応じて,主軸と副軸の添字の対 AX_PRI, AX_SEC を返す. """ if orient in ('x'): ax_pri, ax_sec = 0, 1 elif orient in ('y'): ax_pri, ax_sec = 1, 0 else: com.panic(f'no such orient={ orient }!') return ax_pri, ax_sec #To be Override
[docs] def arrange_box_children(self, **kwargs): """自身の子すべてを再配置する.必ず終わりにself.boxesを設定すること. 次の属性を操作する: * self.children_ * self.boxes """ ## 軸の設定: Primary, secondary ax_pri, ax_sec = PackerBoard._get_axes(self.orient) # 第1回目のパス: 子の包含矩形のサイズの最大と総和を求める _max_shape, _, _ = PackerBoard._accumulate_boxes(self.children_box_enumerated()) # 内部の子ボードの形状サイズ指定 _ishape = [None, None] #内部のさや(pod)の形状サイズ.可変データ # com.ensure(com.is_typeof_seq(_max_shape, etype=(float,int), # dim=2, verbose=True), # f'max_shape={_max_shape} must be a pair of numbers!') com.ensure_point(_max_shape, name='_max_shape', nullable=False) _ishape = _max_shape if self.packing==None: pass # elif self.packing in ('even'): # pass # elif self.packing in ('pack'): # _ishape[ax_pri] = None #primary-axis has unbounded size # pass else: pass # 第2回目のパス _child_pos = [0, 0] #初期値: 子ボードの配置位置 _boxes = EMPTY_RECT #包含矩形の初期値 for idx, pair in self.children_enumerated(): trans, child = pair #分解 #子配置の変換 trans1 = crt.Translate(dest=_child_pos) #変換と子を追加 self.set_child_by_idx(idx=idx, pair=(trans1, child)) #子の配置情報の計算 if True: print(f'@debug00@PackerBoard:child={child.myinfo()} shape=_ishape={_ishape}') child._arrange(shape=_ishape) #再帰的に処理 #自身の包含矩形の計算 box0 = child.get_box() com.ensure(box0 != None, f'box0={box0} is None!') box1 = crt.box_apply_trans(box0, trans=trans1) com.ensure(box1 != None, f'box1={box1} is None!') _boxes = crt.box_union(_boxes, box1) #親の包含矩形の更新 if False: print(f'@debug:pack:update:{self.myinfo()} child={box1} => self={_boxes}') #次の配置位置を求める _old_child_pos = copy.copy(_child_pos) ## 主軸: 子の主軸長だけ,配置位置を進める if _ishape[ax_pri]!=None: _child_pos[ax_pri] += _ishape[ax_pri] else: _child_pos[ax_pri] += crt.box_to_shape(box1)[ax_pri] ## 副軸: 配置位置は変えない. if False: print(f'@debug:pack:orient={self.orient} pos={ _old_child_pos } => pos_new={ _child_pos }') #exp 自身の情報を更新 self.boxes = com.ensure_box(_boxes, name='_boxes', nullable=False) return self.box
pass ##class PackerBoard
#===== # サブクラス #=====
[docs]class GridPackerBoard(PackerBoard): """描画を行う画盤(board)のクラス.`PackerBoard`のラッパー. Args: **kwargs : 他のキーワード引数.上位クラスに渡される. """ def __init__(self, **kwargs): """画盤オブジェクトを初期化する """ #引数 super().__init__(**kwargs) if self.verbose: self.repo(msg=f'{self.myinfo()}.__init__(): { self.vars() }') return
##====== ## 描画演算オブジェクト ##======
[docs]class DrawCommandBase(CoreBoard): """描画演算オブジェクトの基底クラス.Boardクラスのサブクラス. Args: cmd (str) : 命令の名前の文字列.default=None. **kwargs (dict) : 上位コマンドに渡すオプション引数. Attributes: cmd (str) : 命令の名前の文字列.default=None. kwargs (dict) : コマンドの引数からなる辞書.各コマンドは,この辞書の項目を参照して実装する. """ def __init__(self, cmd=None, #命令の名前 **kwargs #命令の引数 ): verbose = kw.get(kwargs, key='verbose') com.ensure(cmd!=None, f'DrawCommandBase(): cmd must not be None!') super().__init__(**kwargs) if verbose: self.repo(msg=f'DrawCommandBase:{self.myinfo()}(): cmd={ cmd } kwargs={ kwargs }') com.ensure(type(cmd) is str, f'cmd={cmd} must be str!') #命令を格納する self.cmd :str = cmd # self.kwargs = kwargs #exp return #Override
[docs] def draw_me_before(self, cr): if self.verbose: self.repo(is_child=True, msg=f'DrawCommandBase:{self.myinfo()}.draw_me_before(): { self.vars() }') kwargs1 = kw.extract(kwargs=self.kwargs, keys=['x', 'y', 'width', 'height', 'fill', 'source_rgb', 'edge_rgb', 'line_width']) self.draw_me_impl(cr) return
#Override: To be implemented
[docs] def draw_me_impl(self, cr): """To be implemented """ return
# #Override # def get_kwargs(self): # return self.kwargs # pass # class DrawCommandTemplate(DrawCommandBase): # """描画演算オブジェクトのクラス.DrawCommandBaseクラスのサブクラス. # """ # def __init__(self, cmd=None, **kwargs): # super().__init__(cmd=cmd, **kwargs) # return #====== # 単一図形描画 #====== CFSIZE=4
[docs]class DrawRectangle(DrawCommandBase): """描画演算オブジェクトのクラス.DrawCommandBaseクラスのサブクラス. Args: x (float) : default=0.0. y (float) : default=0.0. **kwargs (dict) : 上位コマンドに渡すオプション引数. """ def __init__(self, x=0.0, y=0.0, width=None, height=None, **kwargs): self.x = com.ensure_defined(value=x, required=True) self.y = com.ensure_defined(value=y, required=True) # self.width : float = com.ensure_defined(value=width, required=True) # self.height: float = com.ensure_defined(value=height, required=True) super().__init__(cmd='rectangle', **kwargs) _width : float = com.ensure_defined(value=width, required=True) _height: float = com.ensure_defined(value=height, required=True) self.shape = _width, _height return #Override
[docs] def arrange_box_self(self): x, y = self.x, self.y # width, height = self.width, self.height # self.box = (x, y, x+width, y+height) shape = com.ensure_point(self.shape, name='self.shape', nullable=False) self.box = (x, y, x+shape[0], y+shape[1]) return
#Override
[docs] def draw_me_impl(self, cr): shape = com.ensure_point(self.shape, name='self.shape', nullable=False) crt.cr_rectangle(x=self.x, y=self.y, # width=self.width, height=self.height, width=shape[0], height=shape[1], context=cr, **self.kwargs) if kw.get(self.kwargs, 'debug', default=False): crt.cr_text(context=cr, ox=0, oy=0, msg=f'{self.get_trans()}', fsize=CFSIZE) #debug crt.cr_text(context=cr, ox=0, oy=2*CFSIZE, fsize=CFSIZE, msg=f'box_={ self.box }') #debug if kw.get(self.kwargs, 'show_native_origin', default=False): crt.cr_draw_marker_cross(context=cr, x=0, y=0, linewidth=0.5, angle=math.pi*0.0, rgb=crt.cr_add_alpha(crt.MYCOL['blue'], alpha=0.5)) return
[docs]class DrawCircle(DrawCommandBase): """描画演算オブジェクトのクラス.DrawCommandBaseクラスのサブクラス. Args: x (float) : 円の中心位置のx座標.default=0.0. y (float) : 円の中心位置のy座標.default=0.0. r (float) : 円の半径.必須. **kwargs (dict) : 上位コマンドに渡すオプション引数. """ def __init__(self, x=0.0, y=0.0, r=None, **kwargs): self.x : float = x self.y : float = y self.r : float = r com.ensure(self.r != None, f'r={r} is None!') super().__init__(cmd='circle', **kwargs) return #Override
[docs] def arrange_box_self(self): x = self.x y = self.y r = self.r self.box = (x-r, y-r, x+r, y+r) return
#Override
[docs] def draw_me_impl(self, cr): crt.cr_circle(x=self.x, y=self.y, r=self.r, context=cr, **self.kwargs) if kw.get(self.kwargs, 'debug', default=False): crt.cr_text(context=cr, ox=0, oy=0, msg=f'{self.get_trans()}', fsize=CFSIZE)#debug crt.cr_text(context=cr, ox=0, oy=2*CFSIZE, fsize=CFSIZE, msg=f'box_={ self.box }') #debug if kw.get(self.kwargs, 'show_native_origin', default=False): crt.cr_draw_marker_cross(context=cr, x=0, y=0, linewidth=0.5, angle=math.pi*0.0, rgb=crt.cr_add_alpha(crt.MYCOL['blue'], alpha=0.5)) return
#====== # 線分列の描画 #====== #constants CMD_MOVE = 0 CMD_LINE = 1
[docs]class DrawPolyLines(DrawCommandBase): """描画演算オブジェクトのクラス.DrawCommandBaseクラスのサブクラス. Args: **kwargs (dict) : 上位コマンドに渡すオプション引数. Examples:: #サイズが4x4の十字型(X)を書く. P = DrawPolyLine() P.move_to(0.0, 0.0) P.line_to(4.0, 4.0) P.move_to(0.0, 4.0) P.line_to(4.0, 0.0) """ def __init__(self, **kwargs): super().__init__(cmd='polylines', **kwargs) self.commands = [] return #Override
[docs] def arrange_box_self(self): #命令全ての包含長方形を計算する _boxes = EMPTY_RECT for idx, pair in enumerate(self.commands): cmd, x, y, _ = pair #分解 box1 = com.ensure_box((x, y, x, y), name='(x, y, x, y)') _boxes = crt.box_union(_boxes, box1) self.boxes = self.box = _boxes if False: print(f'@debug:polyline: self.box={self.box}') return
#Override
[docs] def draw_me_impl(self, cr): crt.cr_set_context_parameters(context=cr, **self.kwargs) x_last, y_last = None, None for idx, pair in enumerate(self.commands): cmd, x, y, has_arrow = pair #分解 if cmd==CMD_MOVE: if self.verbose: kwargs={ 'x':x, 'y': y, } self.repo(is_child=True, msg=f'@debug: cr_move_to: { kwargs }') crt.cr_move_to(x, y, context=cr) elif cmd==CMD_LINE: if self.verbose: kwargs={ 'x':x, 'y': y, 'has_arrow': has_arrow, 'x_last': x_last, 'y_last': y_last, } self.repo(is_child=True, msg=f'@debug: cr_line_to: { kwargs }') if x_last != None and y_last != None: crt.cr_line_to(x, y, context=cr, x_last=x_last, y_last=y_last, has_arrow=has_arrow, **self.kwargs) else: com.panic(f'PolyLines.draw_me_impl: no such cmd={cmd}!') x_last, y_last = x, y pass crt.cr_process_stroke_or_fill(context=cr, **self.kwargs) return
[docs] def move_to(self, x, y) -> BoardBase: """ペンを位置`(x,y)`に移動する.直線は引かない. Args: x (float) : x- and y-coodinates y (float) : x- and y-coodinates Returns: (Board) : 自分自身.いわゆる'cascade object call interface' のため. """ has_arrow = False self.commands.append((CMD_MOVE, x, y, has_arrow)) return self #for cascade object interface
[docs] def line_to(self, x, y, has_arrow=False) -> BoardBase: """ペンを現在位置から目標位置`(x,y)`まで移動して,直線を引く. Args: x (float) : x- and y-coodinates y (float) : x- and y-coodinates Returns: (Board) : 自分自身.いわゆる'cascade object call interface' のため. """ self.commands.append((CMD_LINE, x, y, has_arrow)) return self #for cascade object interface
pass
[docs]class DrawMarkerCross(DrawPolyLines): """原点 (0,0) を中心とした×印(Cross)を書く. Args: ticklen (float) : ×印の交差辺の長さ. default=4.0. line_width (float) : ×印の交差辺の線幅.default=0.75. """ def __init__(self, **kwargs): self.ticklen = ticklen = kw.get(kwargs, 'ticklen', default=4.0) self.linewidth = linewidth = kw.get(kwargs, key='linewidth', altkeys=['line_width', 'pen_width'], default=0.75) kwargs['linewidth'] = linewidth super().__init__(**kwargs) # clen = 5 self.move_to(0, 0) self.line_to(ticklen*1.0, ticklen*0.0) self.move_to(0, 0) self.line_to(ticklen*(-1.0), ticklen*0.0) self.move_to(0, 0) self.line_to(ticklen*0.0, ticklen*1.0) self.move_to(0, 0) self.line_to(ticklen*0.0, ticklen*(-1.0)) return
#====== # 関数終わり #====== # ## メイン文 # if __name__ == '__main__': # shape = (9,6) # reg = ImageBoard(ratio=128, shape=shape, xy = (0.5, 0.5)) # print('reg: \n', vars(reg)) ##EOF