Open36

Blender関連の雑多なメモ類

nikogolinikogoli
nikogolinikogoli

ぬっち氏の「Blenderプラグインの作り方」の出来が良すぎて、これの2.8対応版があればそれで十分な感じはある

nikogolinikogoli

◇ メニューの追加登録候補について(本体はまとめに移動)

  • ユーザー設定 (USERPREF) はオペレーター関連 (_OT_) だけ PREFERENCEに分離されている
  • メニューの append・prepend・remove は、scripts/modules/bpy_types.py で定義されているので、(たぶん)変更可能
    • → 同名のメニューを自作メニューで置き換えるとかできるかも?
    • appendはこんな感じ
      @classmethod
      def append(cls, draw_func):
          """
          Append a draw function to this menu,
          takes the same arguments as the menus draw function
          """
          draw_funcs = cls._dyn_ui_initialize()
          cls._dyn_owner_apply(draw_func)
          draw_funcs.append(draw_func)
      
    • ちなみに、context.copy()もこのファイルで定義されていた
nikogolinikogoli

オペレーターやクラスのプロパティの情報を取得したい

bpy.types.SubsurfModifierboundary_smoothPRESERVE_CORNERSの説明文や、 bpy.ops.object.data_transfervert_mappingEDGEINTERP_NEARESTの説明文を流用したいが、いちいち API Document をコピペするのはちょっと... というとき

types.Hogeがある場合

struct = bpy.types.SubsurfModifier.bl_rna  #.bl_rna で bpy_struct を取得
prop = struct.properties['boundary_smooth']  # properties["名前"] でプロパティを取得
text = prop.enum_items['PRESERVE_CORNERS'].description  #Enum の場合はこんな感じ

types.Hogeがない場合(オペレーター)

struct = bpy.ops.object.data_transfer.get_rna_type()  #.get_rna_type() で bpy_struct を取得
prop = struct.properties['vert_mapping']  # あとは types.Hoge と同じ
text = prop.enum_items['EDGEINTERP_NEAREST'].description

参考:How can I access view3d.select_circle radius value?

同様にしてstruct.functionsでメソッドが取れた はず

nikogolinikogoli

コピペでは駄目な理由:「同じテキスト(のはず)なのに、なぜか違う訳になる」という現象

例(2.91で発生、2.92は未確認)

適当なアドオンの中
class AddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__

    def draw(self, context):
        txt_solid=bpy.types.UILayout.enum_item_name(context.active_object, 'display_type', 'SOLID')
        self.layout.label(text=txt_solid)  #これは「ソリッド」と翻訳される
        self.layout.label(text="Solid")  #これも「ソリッド」と翻訳される
        self.layout.operator("何かのオペレーター", text=txt_solid)  #これは「ソリッド」と翻訳される
        self.layout.operator("何かのオペレーター", text="Solid")  #これは「立体」と翻訳される

翻訳 context のパラメータ設定が違うのかな?

nikogolinikogoli

◇ Blender のスクリーンショット作成機能とか

  • python のテストではアドオンの機能テストはできるけどパネル表示のテストはできないよね問題の解決に使えないか?
    • と思ったけどやっぱ無理? invoke_default → スクショ → キャンセルってできるんだろうか
  • UI というか各エディターの基本状態の画像一覧をこれで自動的に作りたい
  • あと mouse_gesture でやってるやつ
    • これはスクリーンショット機能使ってなかったかも
nikogolinikogoli

Blender のスクリーンショット作成機能では、UIは映らないっぽい 勘違いだった

  • エディターの基本状態の画像 → できる
  • invoke_default してスクリーンショット → ボタン経由では可能、draw での自動実行は不可能

    Calling operator "bpy.ops.screen.screenshot" error, can't modify blend data in this state (drawing/rendering)

nikogolinikogoli

これに限らず、「invoke でポップアップ出す → 処理を自動で実行する」のはどうすればいいんだろ

  • app.tymer ?
nikogolinikogoli

◇ property の add() というか公式の PropertyGroup Example のやつ

  • bpy.types.PropertyGroupを継承したプロパティグループのクラスMySetを定義
  1. 使い方その1どこか.prop = bpy.props.PointerProperty(type=MySet)とする
    • プロパティグループが1つ設定された状態
    • どこか.prop.nameのようにMySetの各attrの name を使って個別プロパティを取得する
    • これが公式の Exapmle の上のやつ

  2. 使い方その2どこか.prop = bpy.props.CollectionProperty(type=MySet)とする
    • コレクションプロパティが1つ設定された状態 = 設定時点では 枠 だけできて、中身のグループはない
      • len(どこか.prop)=0であり、真偽判定では Falseとして扱われる
      • ただしリストというわけではなく、どこか.prop == [] → False
    • どっか.hoge.add()を行うとコレクションの中にMySetのインスタンス?が追加される
      • add()は引数を取らない → コレクションの中身はアクセスのための固有の識別子?を持てない
      • \Rightarrowどっか.hogeの各要素はどっか.hoge[0]のようにインデックスで取得する
      • あるいは x for x in どっか.hogeのように for で回してあれこれする
    • add()は追加したインスタンスを返すので、item = どっか.hoge.add()としておけばitem.nameのように個別プロパティを取得できる
    • これが公式の Example の下のやつ

要するにどっか.hoge.add()・・・初期設定の何かのプロパティでコレクションを埋めている
  → そのあとにx for x in どっか.hogeがあり、そこで実際のプロパティの値を設定しているはず

nikogolinikogoli

CollectionProperty.add()に関する doc の記述が見つからないんだけど、存在しないんだっけ...?

nikogolinikogoli
properties_object.py
class OBJECT_PT_context_object(ObjectButtonsPanel, Panel):
    bl_label = ""
    bl_options = {'HIDE_HEADER'}

    def draw(self, context):
        layout = self.layout
        space = context.space_data

        if space.use_pin_id:
            layout.template_ID(space, "pin_id")
        else:
            row = layout.row()
            row.template_ID(context.view_layer.objects, "active", filter='AVAILABLE')
  • 通常時はbpy.context.view_layer.objects.activeが判定に使用される (AVAILABLE?)
    • ちゃんと Global Conetxt の要素(view_layer)を使用している
    • = (定義?上は) Local な active_objectの有無には依存していない
  • ピン留めされたときはbpy.context.space_data.pin_idにあるidが判定に使用される
    • パネル表示処理?上ではcontext.objectは重要でない
    • ピン留めはpin_idに『何』を投げているのか?
      • そもそも投げていない = bpy.context.view_layer.objects.activepin_idにセット の可能性が高そう
nikogolinikogoli

bpy.types.UILayout.context_pointer_set

以下のようにして使う (ここの真似)

class MyMenu(bpy.types.Menu):
	bl_label = "My Menu"
	bl_idname = "どこか.my_menu"
		...
	def draw(self, context):
		row = self.layout.row()
		row.context_pointer_set("my_camera", camera) #row() や column() に使う
		row.menu(何か別のメニュー.bl_idname)
		row.operator(何かのオペレーター.bl_idname)
C のコード
source/blender/editors/interface/interface_layout.c$5620
void uiLayoutSetContextPointer(uiLayout *layout, const char *name, PointerRNA *ptr)
{
  uiBlock *block = layout->root->block;
  layout->context = CTX_store_add(&block->contexts, name, ptr);
}
  • 上記の場合、rowにセットされたmenuoperatorにおいて、context.my_cameraが使用可能になる
  • あくまでもrowの中に入らないと有効にならないようで、上記のMyMenu内でrowをいじってもcontext.my_cameraは使えない
  • 基本はmenuで使うもののようで、operator内でcontext.my_cameraを呼ぶとよくわからない挙動
    1. 直接executeが動くもの:context.my_cameraが使える
    2. drawするもの:最初のdraw実行時は使えるが、再度drawするとcontext.my_cameraがなくなっていてAttributeErrorになる
    3. drawしてOKボタン等でexecuteするもの:executeするときにはcontext.my_cameraがなくなっていてAttributeErrorになる
    4. invokeしてexcuteするもの:invoke時にはcontext.my_cameraは存在するが、excute時にはなくなっている
  • そもそもオペレーターは普通にoprator().camera = camera すれば良い

で、書いてて思ったけど、これbpy.types.Menuがプロパティを持てない問題への対処法なのでは?

  • 上のリンクも "How to create submenus with dynamic content?" への解答としてcontext_pointer_setが提示されているわけだし
  • \Rightarrow オペレーターのように.menu(サブメニュー).content = targetができないので、代替としてcontext_pointer_set("content", target)を行う と考えると綺麗
nikogolinikogoli

ぬっち氏の「BLENDER INTERNAL」の記述 (p.68) によると、モーダルモード移行時に context の再取得(というか再設定?)が行われている。

  • invoke()や最初の draw()では実行時に取得した context が利用され、その後はモード移行時に取得した context が利用される挙動と思われる。
    • モード移行時の context には UIlayout は含まれていないため、そこで付加された custom context も失われてしまう...のか? ほんとに?
nikogolinikogoli

別ウィンドウでテキストエディタを出し、出力を貼り付けるオペレーター

def execute(self, context):
	if len(context.window_manager.windows) == 1:
		bpy.ops.screen.userpref_show()
	text_area = context.window_manager.windows[-1].screen.areas[-1]
	text_area.type = 'TEXT_EDITOR'
	if "temp_text" in bpy.data.texts:
		text_area.spaces[0].text = bpy.data.texts["temp_text"]
	else:
		text_area.spaces[0].text = bpy.data.texts.new(name="temp_text")
	pre_clipboard = context.window_manager.clipboard
	override = context.copy()
	context.window_manager.clipboard = "\n".join(dir(bpy.types)) # textで見たい情報
	override['area'] = text_area
	bpy.ops.text.select_all(override)
	bpy.ops.text.paste(override)
	bpy.ops.text.jump(override, line=1)
	return {'FINISHED'}
  • 別にクリップボードを介する必要はなくて、text_area.spaces[0].text.write("\n".join(dir(bpy.types)))すればいいだけなんだけどね
nikogolinikogoli

bl_rna_get_subclass_py:クラス名からクラスオブジェクトを取得する 参考

プロパティエディタのタブを取り出したポップアップを出す
def invoke(self, context, event):
	return context.window_manager.invoke_props_dialog(self)

def draw(self, context):
	areas = context.window_manager.windows[0].screen.areas
	for a in areas:
		if a.type == "PROPERTIES":
			sp_data = a.spaces[0]
	def myEmpty(data):
		myEmpty.space_data = sp_data
		return None
	myEmpty(sp_data) # ↓ "_PT_"なら Panel のように、引数と合わせる
	clas = bpy.types.Panel.bl_rna_get_subclass_py('PROPERTIES_PT_navigation_bar')
	clas.draw(self, myEmpty) # draw 内で attr として xx.space_data が呼ばれる 辞書では駄目
nikogolinikogoli

もうちょっとちゃんとした手順というかbl_rna_get_subclass_pyを使う流れ

# my_addon.py の中でオペレータを定義
# class MyOprator(bpy.types.Operator):
#     bl_idname = "wm.my_operator"

op_struct = bpy.ops.wm.my_operator.get_rna_type()  # get_rna_type() で bpy_struct を取得
    # op_struct: <bpy_struct, Struct("WM_OT_my_operator") as 0x.... >
    #  ↓
op_cls_in_types = op_struct.__class__  # __clas__ で Blender 内部でのクラス?を取得
    # op_cls_in_types: <class 'bpy.types.WM_OT_my_operator' >
    #  ↓
op_cls_name = op_cls_in_types.__name__  # __name__ で クラス名を str として取得
    # op_cls_name: WM_OT_my_operator
    #  ↓
op_class = bpy.types.Operator.bl_rna_get_subclass_py(op_cls_name)
   # op_class: <class 'my_addon.MyOperator' >

op_class まで到達すると、AttributeErrorを出さずに、op_class.xxのようにクラス変数にアクセスしたり、op_class.draw()のようにメソッドを呼んだりできる

nikogolinikogoli

ユーザーが追加したオペレータでは上記の手順でクラスオブジェクトの取得に成功できる
しかし、デフォルトのオペレータでは name は取れても bl_rna_get_subclass_py(name)Noneになってしまう

nikogolinikogoli

めも

  • Topbar の space_data は それ用のサブクラスがなく Space クラスがそのまま使われている
  • PoseBone.bone == 正しいBoneTrue BUT PoseBone.bone is 正しいBoneFalse
  • 座標に関連する複数のプロパティ
    • matrix: Final 4x4 matrix after constraints and drivers are applied (object space)
    • matrix_basis: Alternative access to location/scale/rotation relative to the parent and own rest bone
    • matrix_channel: 4x4 matrix, before constraints
    • とりあえず、ポーズボーンが変化していない(レスト位置のまま)のときは
      • PoseBone のmatrix ≒ EditBone の matrix
      • PoseBone のmatrix_basis → 単位行列
      • PoseBone のmatrix_channel → ?

nikogolinikogoli

context.objectcontext.active_objectの違いの話

公式:context.objectはボタン/パネル用だよ (超意訳)
  ↓
プロパティエディタ(Button Cont.)が context.objectを持つのは理解できる
では、なぜプロパティエディタの外、つまり Screen Cont. にも context.objectがあるのか?
= 3Dビューでのcontext.objectcontext.active_objectの、想定されている使い分けとは?
  ↓
3Dビューにもパネルはある・・・Screen Cont. のcontext.objectは、サイドバーなどで利用するためでは?
  ↓
予想__PT__系は .object(もしかしたら__MT__系も) 、__OT__系は.active_objectを使うのが公式の作法

space_view3d.py の各クラスでの `context.object` /` active_object` の使い分け
クラス名 context.object context.active_object
VIEW3D_HT_tool_header def draw_mode_settings
_draw_tool_settings_context_mode: def PAINT_GPENCIL
VIEW3D_HT_header def draw
VIEW3D_MT_editor_menus def draw
VIEW3D_MT_transform_armature def draw
TOPBAR_MT_edit_curve_add def draw
VIEW3D_MT_object def draw
VIEW3D_MT_object_context_menu def draw
VIEW3D_MT_hook def draw
VIEW3D_MT_vertex_group def draw
VIEW3D_MT_gpencil_vertex_group def draw
VIEW3D_MT_pose_group def draw
VIEW3D_MT_edit_mesh_context_menu def draw
VIEW3D_MT_edit_mesh_extrude def draw
VIEW3D_MT_edit_font_kerning def draw
VIEW3D_MT_assign_material def draw
VIEW3D_MT_gpencil_copy_layer def draw
VIEW3D_MT_gpencil_animation def poll
VIEW3D_MT_pivot_pie def draw
VIEW3D_PT_snapping def draw
VIEW3D_PT_overlay_gpencil_options def draw
VIEW3D_PT_context_properties def _active_context_member
VIEW3D_PT_gpencil_draw_context_menu def draw
TOPBAR_PT_gpencil_materials def poll
TOPBAR_PT_gpencil_vertexcolor def poll
せめて公式は統一してくれよ・・・
nikogolinikogoli

どっちかだけで良くない?

source/blender/editors/screen/screen_context.c
static eContextResult screen_ctx_active_object(const bContext *C, bContextDataResult *result)
{
  wmWindow *win = CTX_wm_window(C);
  ViewLayer *view_layer = WM_window_get_active_view_layer(win);
  Object *obact = view_layer->basact ? view_layer->basact->object : NULL;

  if (obact) {
    CTX_data_id_pointer_set(result, &obact->id);
  }

  return CTX_RESULT_OK;
}
static eContextResult screen_ctx_object(const bContext *C, bContextDataResult *result)
{
  wmWindow *win = CTX_wm_window(C);
  ViewLayer *view_layer = WM_window_get_active_view_layer(win);
  Object *obact = view_layer->basact ? view_layer->basact->object : NULL;

  if (obact) {
    CTX_data_id_pointer_set(result, &obact->id);
  }

  return CTX_RESULT_OK;
}
nikogolinikogoli
poll 関連
source/blender/python/intern/bpy_operator.c$246
if (WM_operator_poll_context((bContext *)C, ot, context) == false) {
    const char *msg = CTX_wm_operator_poll_msg_get(C);
    PyErr_Format(PyExc_RuntimeError,
                 "Operator bpy.ops.%.200s.poll() %.200s",
                 opname,
                 msg ? msg : "failed, context is incorrect");
    CTX_wm_operator_poll_msg_set(C, NULL); /* better set to NULL else it could be used again */
    error_val = -1;
  }
source/blender/windowmanager/intern/wm_event_system.c$855
/* sets up the new context and calls 'wm_operator_invoke()' with poll_only */
bool WM_operator_poll_context(bContext *C, wmOperatorType *ot, short context)
{
  return wm_operator_call_internal(C, ot, NULL, NULL, context, true, NULL);
}
nikogolinikogoli

API Doc. の UILayout のページに、icon 用のスクロールが導入された

良かった・・・公式も問題だと認識していたんだね・・・

こんな感じ(画像)   ↓

ページ全体のスクロールバーの大きさを比べてみた感じ、2.91版の3分の2ぐらいになった?

nikogolinikogoli

ドライバーを使ったコンストレイントのオーナー空間の制御

というか Enum Property の制御

Enum Property はインデックスを使うことで eunm_items の要素を取得できる
なので、ポーズボーン.constraints["Limit Rotation"].owner_spaceにドライバーを設定し、
0~3 の整数値を取るようにすれば、オーナー空間の設定を変更するスイッチっぽいものが作れる

ただし、ボーン以外では eunm_items が"World" と "Local" に減る関係なのか、

  • Blender 内の表示:[ワールド、ポーズ、親付きローカル、ローカル]
  • 実際の並び:[World, Local, Pose, Local_with_parent][1]

のように見た目と中身が違う

とりあえず、画像のようにインデックスの配列を作って対処することにした

脚注
  1. 'CUSTOM'がどう処理されているのかは不明 ↩︎

nikogolinikogoli

directly terminate a modal operator through the API 問題

あるいは、 invoke したポップアップを script 経由で閉じることはできるか 問題

いまのところの結論: ○「閉じるだけ」 ×「閉じて再度開く」

わかってること:

  • ctypes.windll.user32.keybd_event(0x1B) Esc キーイベントを起こす
    → 一応閉じれたはず
    \Leftrightarrow オペレーター自体が止まってしまうので、もう一度自動的に invoke することができない
  • invoke の中では、オペレーターの invoke はスキップされる
    → コンテクストメニューでは ポップアップがでない問題
    ※ なぜか menu を経由するとポップアップが出るので、invoke → menu → invoke は行けるかも?
  • bpy.types.Window.event_simulate
    ここのコメントによると、↓で、debug 用の機能らしく、add-on には向いていないとのこと

    You can start Blender with the CLI argument --enable-event-simulate which allows you to simulate events through bpy.context.window.event_simulate("ESC", "PRESS")

  • invoke_xx 系を return 以外で呼ぶと、ポップアップ自体は出る
    → modal モードに入ってタイマーを使って呼ぶと、draw 内容が変更されたポップアップが出せる
    \Leftrightarrow すでに出ているポップアップが閉じるわけではない
  • invoke_xx 系を return 以外で呼ぶと、閉じる処理がうまく行かないっぽい
    → ポップアップを出したあとになにかすると、 ACCESS_VIOLATION でクラッシュする
nikogolinikogoli

結局、↓ なんだよね・・・

  • invoke でモーダルモード
    何かしらの処理の自動実行ができるが、ポップアップをうまく扱えない
  • invoke でポップアップ
    ポップアップを扱えるが、モーダルモードではないらしく処理の自動実行が不可
nikogolinikogoli
  • モーダル入る → ポップアップ出す → スクリーンショット取って保存 → esc イベントで停止
    CANCELLED になっても外部への保存は巻き戻されない気がするし、いける?
     esc まではできそうだが、そのあと何かすると Blender が落ちるので、だめ

  • プロパティに update を設定して prop で表示させると、常に update が呼ばれ続ける
    今確かめたら、再発しない・・・なぜ?
    → update の中でスクショ、いける?

nikogolinikogoli

アドオンで登録した class の __init__()

  • AddonPreferences では、有効化/無効化 の切り替えでは呼ばれない
    → 設定関係で 初期化する場合は、register()で行う必要があるっぽい
  • オペレーターでは、呼ばれた際に、invoke より先に行われる
    と思っているけど、上の結果を見ると勘違いな気がしてきた
nikogolinikogoli

プロパティやドライバーの話

  • オペレーターや AddonPref. で設定した XxProperty 系は、IDTYPE_FLAGS_NO_ANIMDATAによってアニメーションが禁止されている
    → なので、ドライバーを設定できない(driver_add すると not animatableエラーが出る)
    ※ 確かめていないが、たぶんパネルも同じ

疑問
スクリプトで設定したユーザー定義プロパティが、カスタムプロパティに表示されたりされなかったりするのはなぜか。 というか、そもそも「表示される」ものなのか。

  • カスタムプロパティの表示はrna_prop_ui.pyで定義されていて、例えばobject.items()の返り値を元に表示が作られる
    → 中身は (str, IDPropertyGroup) のリスト
    \Leftrightarrow この形式のプロパティでは、参照するデータが同じだがタイプが違うもの(というか IDProp. が指している本体?)をカスタムプロパティの持ち主が持っている
    = つまり、object["cycles"]object.cyclesもOK
    ※ 結局のポインタ値 (data) は同じだが「入れ物」は違うので、同列に扱ってはいけない気はする

  • しかし、ユーザーが定義したカスタムプロパティは (str, float)であり、rna_idprop らしいがここで完結している
    = つまり、object["prop"] はあるが、object.propはない

  • dskjalさんの説明にあるように、値が変更されるとユーザー定義プロパティが突然カスタムプロパティに表示される
    → IDPropertyGroup の設定がユーザー定義プロパティの読み込みより早いので、値の更新か何かで IDPropertyGroup が再設定されるまでは反映されない?


nikogolinikogoli

ユーザー定義のプロパティにドライバーを設定する

適当なオペレーター
def execute(self, context):
	driver = bpy.data.objects["何か"].driver_add("プロパティ名").driver
	driver.type = 'SCRIPTED'
	driver.expression = "var + 1"
	if not driver.variables:
		dr_var = driver.variables.new()
	else:
		dr_var = driver.variables[0]
	dr_var.type = 'TRANSFORMS'
	tg = dr_var.targets[0]
	tg.id = bpy.data.objects["ターゲットの名前"]
	tg.transform_type = 'LOC_Z'
nikogolinikogoli

いまさら知った description の動的な設定

class SpaceSwitch(bpy.types.Operator):
    ...
    ident : StringProperty(name="enum identifier", default="")
    desc_dic = {i.identifier: i.description
        for i in bpy.types.Constraint.bl_rna.properties["owner_space"].enum_items}

    @classmethod
    def description(cls, context, properties): 
        key = properties.ident  # 'WORLD' / 'POSE' / 'LOCAL_WITH_PARENT' / 'LOCAL'
        return bpy.app.translations.pgettext_tip(cls.desc_dic[key])
  • 2.91 では operator の text では label とは異なる翻訳辞書らしく、訳されないことがある
    bpy.app.translations.pgettext_tipで明示的に翻訳を適用
  • 通常のプロパティは properties.xxxでアクセスし、クラス変数はcls.yyyでアクセスする
nikogolinikogoli

フェイクユーザーの話

このスクラップやそのリンク先を読んでいて、せっかくなので C を見てみた

コード
source/blender/blenkernel/intern/lib_id.c
/* $324  */
void id_fake_user_set(ID *id)
{
  if (id && !(id->flag & LIB_FAKEUSER)) {
    id->flag |= LIB_FAKEUSER;
    id_us_plus(id);
  }
}
/* ↓ id_us_plus() の中で呼ばれているものの1つ*/
/* $203  */
void id_lib_extern(ID *id)
{
  if (id && ID_IS_LINKED(id)) {
    BLI_assert(BKE_idtype_idcode_is_linkable(GS(id->name)));
    if (id->tag & LIB_TAG_INDIRECT) {
      id->tag &= ~LIB_TAG_INDIRECT;
      id->flag &= ~LIB_INDIRECT_WEAK_LINK;
      id->tag |= LIB_TAG_EXTERN;
      id->lib->parent = NULL;
    }
  }
}
source/blender/makesdna/DNA_ID.h$285
  /**
   * LIB_... flags report on status of the data-block this ID belongs to
   * (persistent, saved to and read from .blend).
   */
  short flag;
  /**
   * LIB_TAG_... tags (runtime only, cleared at read time).
   */
  int tag;

ぱっと見た感じ、ID.flag と ID.tag の値の変更が中心に見える
どっちかを参照して保存するデータを決めているんだろうか?

nikogolinikogoli

"the data-block this ID belongs to" でデータブロックと ID が別扱いなのがわかんない

  • 「struct → data = ID → library」って構造っぽい
  • append の説明 "Append from a Library .blend file" を見ると、「library = .blend file」?
  • で、ディレクトリ内の個別の要素がデータブロック?
source/blender/makesdna/DNA_ID.h
/* $273 */
typedef struct ID {
  ...
  struct Library *lib;
  ...
} ID;

/* $348 */
typedef struct Library {
  ID id;
  struct FileData *filedata;
  ...
} Library;

循環参照っぽいんだけど・・・ どっちが底なんだ

nikogolinikogoli

久々に blender 触ったら、3.2 でEVT_TWEAKが削除されていた...

Tweak events have been removed from KeyMapItem.type. (EVT_TWEAK_L, EVT_TWEAK_M, EVT_TWEAK_R)
Instead of special types, regular mouse button types should be used (LEFTMOUSE, MIDDLEMOUSE, RIGHTMOUSE) with the KeyMapItem.value set to CLICK_DRAG. Directional dragging can be specified by setting KeyMapItem.direction.

以前のように event だけ取得して後で direction を解釈して... 的な処理はできなさそう
普通にできた。というか公式のKeyMapItem.directionのほうが判定が厳しすぎて使いづらい