Open10

context.temp_override 周りのあれこれ

nikogolinikogoli

モチベ

Blender 3.2 において API に追加されたbpy.context.temp_override()に関するあれこれ

  • なんか
  • いろいろ
  • 思いついたら
nikogolinikogoli

API 追加の起点となったのは以下のエラー報告
https://developer.blender.org/T92464
terminal 起動というかバックグラウンド起動状態の blender において、特定のオペレータが poll のチェックを通過できないという話。

  • ちゃんと確認していないが、バックグラウンド起動状態では window が存在しないためそこに連なる context 要素も作成されず、結果としてactive_objectが取得できないために poll failed になる、話っぽい
  • 対策として、この変更で script 経由で開いた blender の window を context に含めるようになったが、別のエラーを発生させたために変更は revert された
  • で、別の対処法として、API context.temp_override()の追加が提案された、という流れ

従来型で対処できないケースなのかは不明。

nikogolinikogoli

context の override と temp_override

python コンソールやスクリプト経由でオペレーターを実行する場合、RuntimeError: bpy.ops.xxxx.poll() failed, context is incorrectの発生を回避することが必要になる。

その手段の1つが「ユーザー側での context の差し替え」であり、以下のように行われてきた。

override = bpy.context.copy()
override["xxx"] = yyy
bpy.ops.change_xxx(override)

新 API bpy.context.temp_overrider()は、この差し替えの"正式"な手法として提供されている[1]
以下のコメントを考慮すると、従来型の override は将来的に削除されるらしい。

Proposed solution is fine, but should be extended to cover also the 'random keyword' context override currently provided by the override_context positional argument of operator calls, Then this later option can be deprecated and removed in a few releases.

脚注
  1. 実際、3.2 以降で従来型の override を行うと DeprecationWarning が出る ↩︎

nikogolinikogoli

temp_override の "error" の話

https://developer.blender.org/rBf438344cf243632e497772cf1f855b9c8856fd37

概要:object を触るオペレーターにおいて、temp_override()が機能するものと機能しないものがある

object は存在するが未選択であり、context.active_object = Noneである状態において、以下のスクリプトを実行すると、.delete() の poll は成功するが、.convert() の poll は失敗する、という話。

import bpy
obj = bpy.data.objects["Cube2"]
with bpy.context.temp_override(active_object = obj, object = obj, selected_objects = [obj]):
    bpy.ops.object.convert(target='MESH') # convert.poll() は失敗する
    bpy.ops.object.delete() # delete.poll() は失敗しない
nikogolinikogoli

とりあえず、別スクラップから移し替え


これ結構面白い。(ただ、delete はたぶん適切な比較対象ではない)

  • bpy.ops.object.delete() の場合、poll 関数は ED_operator_objectmode(bContext *C)
    delete() の poll
    blender/editors/screen/screen_ops.c$186
    bool ED_operator_objectmode(bContext *C)
    {
      Scene *scene = CTX_data_scene(C);
      Object *obact = CTX_data_active_object(C);
    
      if (scene == NULL || ID_IS_LINKED(scene)) {
        return false;
      }
      if (CTX_data_edit_object(C)) {
        return false;
      }
    
      /* add a check for ob->mode too? */
      if (obact && (obact->mode != OB_MODE_OBJECT)) {
        return false;
      }
    
      return true;
    }
    
  • bpy.ops.object.convert() の場合、poll 関数は object_convert_poll(bContext *C)
    convet() の poll
    blender/editors/object/object_add.cc$2811
    static bool object_convert_poll(bContext *C)
    {
      Scene *scene = CTX_data_scene(C);
      Base *base_act = CTX_data_active_base(C);
      Object *obact = base_act ? base_act->object : nullptr;
    
      if (obact == nullptr || obact->data == nullptr || ID_IS_LINKED(obact) ||
          ID_IS_OVERRIDE_LIBRARY(obact) || ID_IS_OVERRIDE_LIBRARY(obact->data)) {
        return false;
      }
    
      return (!ID_IS_LINKED(scene) && (BKE_object_is_in_editmode(obact) == false) &&
              (base_act->flag & BASE_SELECTED));
    }
    

この2つの poll には、以下のような違いが確認できる。

  • delete()の poll 関数は active_object そのものを取りに行く
  • convert() の poll 関数は active_base の要素としての object を取りに行く

で、poll 関数が実際に何をおこなっているのかを確認すると、

  • delete の poll の中の CTX_data_active_object(C)
    blender/blenkernel/intern/context.c$1354
    struct Object *CTX_data_active_object(const bContext *C)
    {
      return ctx_data_pointer_get(C, "active_object");
    }
    
  • convert の poll の中のCTX_data_active_base(const bContext *C)
    blender/blenkernel/intern/context.c$1359
      struct Base *CTX_data_active_base(const bContext *C)
    {
      Object *ob = ctx_data_pointer_get(C, "active_object");
    
      if (ob == NULL) {
        return NULL;
      }
      const Scene *scene = CTX_data_scene(C);
      ViewLayer *view_layer = CTX_data_view_layer(C);
      BKE_view_layer_synced_ensure(scene, view_layer);
      return BKE_view_layer_base_find(view_layer, ob);
    }
    

ctx_data_pointer_get(C, "active_object")は「"active_object"を指定したctx_data_get」であり、これは『context の辞書 → UI → region → area → screen の順に "active_object" を検索する処理』となる


つまり、"active_object" に対する直接的な処理において、以下の違いがある

  • override した context の辞書から active_objectを取得することは両方で行われている
    • delete の場合:そのまま return する
    • convert の場合:ViewLayer と紐づけて active を確認する処理に回される

その結果、「参照名を "active_object"に貼り替えただけで内部の関係性は書き換えない」temp_overrideでは上書きできない部分が参照されることで、convert の poll は失敗に終わってしまう。


ざっくりまとめ

temp_override( active_object = obj )は、

  • "active_object" に紐づいたオブジェクト」(= 辞書の value) を上書きするが
  • "active" なオブジェクト」(= 実際の関係性) は上書きしない

なので、どれだけ override を駆使しても、実際に active な object をチェックする poll は必ず失敗に終わってしまう[1]

脚注
  1. バグとは言えないと思うが、オブジェクトを触る処理において、『実際に active かどうか』を気にするもの / しないものが混在している状態が良くないのは確かだと思う。公式はこのあたり適当というかほぼ放置で、これまではユーザーの間で「convert するなら事前に select する処理を書きましょう」みたいな情報が共有されることで有耶無耶にされてきた。が、temp_overrideという明確なツールが提供されてユーザーが「temp_override すれば全てうまくいく」という思い込みを持ってしまった結果、オペレータ周りの処理があまり一貫的ではないという Blender の性質が問題として表面化した、という感じ。それはそれとして、実際のところ従来の override でも失敗していたので『問題は起きていない』のだが、これが『temp_overrideのバグ』として報告されるのは「非公式が公式化した途端にユーザーの文句が増える」現象っぽくってちょっともやる。まあ自業自得なんだけど。もっとちゃんと poll の説明してほんとに。 ↩︎

nikogolinikogoli

で、その後


なんか微妙に勘違いしている気がしてきた。from ここ

While not opposed to the general idea here, I wonder why current context override is not working/good enough in that case?

Two reason:

  • Those overrides are only used when calling operators, it's possible we may want to set the context outside of operators, although we could support this exact style of overriding context members with a context manager (without actually setting the active window, area... etc).
  • These set up overrides that work by accessing from the context will override. e.g. CTX_data_active_object(C) where as using BASACT(view_layer) wont.
    In principle it could be argued that's the context should always be used removing the need for this feature.
    In practice there is enough code that uses direct access to window/screen/scene, e.g. WM_window_get_active_scene(C) / WM_window_get_active_screen(C) .. etc.
    Temporarily swapping out windowing context is done quite a lot in Blender's C code

2点目の記述からすると、context から取得される要素 / "親"経由で取得される要素が混合する状況はちゃんと問題として認識されているわけで、そうすると現状のtemp_overrideの挙動は実は"バグ"寄りなのか?