C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

Electron アプリに electron-store で設定ファイル機能を追加する場合の example & tips 的なメモ

GitHub - sindresorhus/electron-store: Simple data persistence for your Electron app or module - Save and load user preferences, app state, cache, etc を使うメモです。

public/conf.js とか適当に作る例:

const path = require("path");
const store = require("electron-store");

const store_conf = {
 // https://www.npmjs.com/package/electron-store#schema
 schema: {
  // 'foo'でアクセスできる
  foo: {
   type: typeof 0,
   maximum: 100,
   minimum: 1,
   default: 50,
  },
  // 'bar'でアクセスできる
  bar: {
   type: typeof "",
   format: "url",
   default: "https://example.com/",
  },
  // 'window' でアクセスできる Object
  window: {
   type: typeof {},
   // Object は明示的に default: {} を与えておかないと子要素のdefaultがあっても取り出せないとか起こるので注意します
   default: {},
   // Object の子要素は properties で定義します
   properties: {
    // 'window.width' でアクセスできる
    width: {
     type: typeof 0,
     maximum: Number.MAX_SAFE_INTEGER,
     minimum: 480,
     default: 960,
    },
    // 'window.height' でアクセスできる
    height: {
     type: typeof 0,
     maximum: Number.MAX_SAFE_INTEGER,
     minimum: 270,
     default: 540,
    },
   },
  },
 },
 // https://www.npmjs.com/package/electron-store#migrations
 migrations: {},
};

module.exports = new store(store_conf);
  • Note:
    • schamaJSON で表現できるけれど、JSONで分離すると key にわざわざ " を付けなければならないとか Number.MAX_SAFE_INTEGERtypeof ''のような定義方法を使えない制限が生じて不便かもしれません。分離する場合も .js にしておくと便利が良さそうです。
    • schema に Object を入れ、その子要素へ default を設定したい場合は親となる Object で default: {} しておかないと子要素の default を取り出せなくなるので注意する必要があります。

public/conf.js を使う例( public/electron.js エントリーポイントを想定 ):

const electron = require("electron");
const app = electron.app;
const browser_window = electron.BrowserWindow;

const conf = require("./conf.js");

let main_window;

function createWindow() {
 app.allowRendererProcessReuse = false;

 main_window = new browser_window({
  width: conf.get("window.width"),
  height: conf.get("window.height"),
  webPreferences: { preload: `${__dirname}/preload.js` },
 });

 // (...abbr...; 後略)

Electron アプリに electron-localshortcut で非グローバルでMenuのアクセラレーターも使わないキーバインドを追加するメモ

準備:

実装例(public/electron.js):

const electron = require("electron");
const app = electron.app;
const browser_window = electron.BrowserWindow;

const local_shortcut = require("electron-localshortcut");

let main_window;

function createWindow() { 
  // (...abbr...; 中略)
}

// (...abbr...; 中略)

app.on("ready", () => {
 createWindow();
 local_shortcut.register(main_window, "F12", () =>
  main_window.webContents.toggleDevTools()
 );
});

// (...abbr...; 後略)

参考

  • javascript - Electron does not listen keydown event - Stack Overflow
    • Note: 今回のメモの直接の参考です。
  • Keyboard Shortcuts | Electron
    • Note: 公式情報として参考にはなるけれど electron-localshortcut を使う方法は紹介されていません。グローバルショートカット(ウィンドウが非アクティブでも反応する)、Menuのアクセラレーター機能(メニューに項目を作る必要が生じる)、レンダラープロセス側でにゃんにゃん(若干面倒&今回ケースの用途では直接レンダラープロセス側にコードをごりごりするのは微妙に美しくない気がしたので今回は↑の方法へ)

Electron アプリに i18next を追加して i18n/l10n 対応するメモ

基本的な使用の流れ

  1. yarn add i18next i18next-node-fs-backend
  2. public/i18n.js など適当に作り
  3. l10n 文字列を取り出したいソースで const i18n = require('./i18n.js') して
  4. 必要なら await i18n.changeLanguage('ja-JP') のように l10n 先を実行時に設定する仕込みをして
  5. i18n.t('hoge') とすると i18next で l10n した文字列を取り出せます
  6. i18next の options で渡す loadPathJSON{ "hoge": "ほげ" } のように key-value 的に l10n リソースを定義追加します。saveMissingtrue にしておけば addPath で与える missing 用のファイルへ l10n リソースが未定義で実行時に要求された文字列のリストが保存されます。

public/i18n.js

  • Note
    • ファイル名や配置はお好みで。
    • optionsConfiguration Options - i18next documentation を見て適当に。
    • fallbackLng を定義すると、 missing による add 動作先も fallbackLng になりました。未設定にしておけば missing による add は dev.missing.json のように addPath で与える {{lng}}dev になります。
const i18n = require("i18next");
const i18n_backend = require("i18next-node-fs-backend");
const path = require("path");
// const is_dev = require("electron-is-dev");

const options = {
 // debug: is_dev,
 lng: "en-US",
 initImmediate: false,
 backend: {
  loadPath: path.resolve(__dirname, "..", "i18n/{{lng}}.json"),
  addPath: path.resolve(__dirname, "..", "i18n/{{lng}}.missing.json"),
  jsonIndent: 2,
 },
 saveMissing: is_dev,
 // fallbackLng: "en-US",
 whiteList: [
  "en-US",
  "ja-JP",
  //, "ko-KR"
  //, "zh-CN"
  //, "zh-TW"
  //, "zh-HK"
 ],
};

i18n.use(i18n_backend);

if (!i18n.isInitialized) i18n.init(options);

module.exports = i18n;

i18n/ja-JP.json の例

  • Note: ファイル名と配置は i18next へ与える options で任意に設定できるのでお好みで。
{ "hoge": "ほげ" }

参考

Inkscape-1.0: 新規作成されるSVGドキュメントの背景色のデフォルト値を変える方法; ダークモードのテーミング&defaultドキュメント背景の設定方法のメモ

Inkscape-1.0 では Theme 機能が実装されたので、 Edit -> Preferences -> Interface -> Theme を設定するとダークモードに優しいテーミングができるようになりました。嬉しいです。しかし、闇の世界に住む私たちの目にInkscapeの画面で最も眩しくて困る部分はドキュメント本体の背景色です。白い画面は疲れます。真っ黒なのも黒線が完全に埋没してしまいますし、初期値は暗い灰色が好みです。これはいまのところ Preferences からではなく、ドキュメント新規作成時のテンプレートを直接変更する必要があります。

  • C:\Program Files\Inkscape\share\inkscape\templates (Windowsのデフォルトのインストールパスを用いている場合)

default.svg (InkscapeのUIをen-USで使用している場合) があるので、管理者権限の適当なテキストエディターで開いて svg/sodipodi:namedview[pagecolor]#FFFFFF からお好みの色へ変更して保存します。

GNU/Linuxでは /usr/share/inkscape/... とかそのあたりに templates があるかもしれません。 en-US 以外の言語をInkscapeのUIに設定している場合は default.ja.svg (日本語用) のように言語が付いたバージョンの templates/default.??.svg を編集します。

f:id:USAGI-WRP:20200506063817p:plain

心穏やかに闇のInkscapeを使えるようになります👍

参考

Blender: 正規表現でがさっと Vertex Group(s) をタゲって統合しつつ元は削除して置き換えられる Scripting: usagi/blender-merge-vertex-groups.py のメモ

前回のメモで使用した p2or/blender-merge-vertex-groups.py を元に:

  1. 正規表現 regex 設定で Vertex Group(s) をまとめてタゲれて
  2. 統合元になった Vertex Group(s) は削除する replace モードを True/False 設定できて
  3. 統合先を明示的な名付け to またはそれが未設定なら "+".join できて
  4. 削除と作成について簡単なログを吐いてくれる
  5. dryTrue/False して dry-run (実際には操作しないけど今の設定で実行するとこれこれが remove でこんな merge ができるよ、を表示だけ)してくれる (2020-04-14追記)

ように改造した Fork:

を作りました💪 便利ですよ✨

f:id:USAGI-WRP:20200413230555p:plain

f:id:USAGI-WRP:20200413230529p:plain

連続で複数パターンを操作する場合は add-on にするならちゃんとGUIを付けないと使いにくそうだし、 Scripting で regex, replace, to を逐次書き換えながら使ってね、でもとりあえずの便利には十分かなって思います。

(2020-04-14追記:↓dry-run)

f:id:USAGI-WRP:20200414011825p:plain

Blender: Armature の削除や変更で Vertex Group との整合性が狂ってしまったときの修正方法、のメモ

状況

「Armature を節約しなきゃ→簡単に Merge できるところは Merge!→ Merge が難しいところは Delete / Disolve ! 」

とかした後に、

「 Armature を Delete / Disolve した部分に対応していた Vertex Group と残した Armature との対応が壊れてしまった…再ペイントは…しんどいでござる」

になった時、 Weight Paint で塗り絵せずに残っている Vertex Group をにゃんにゃんしてうまいことする方法のメモです。

例えば、

f:id:USAGI-WRP:20200413205449p:plain

↑ Armature は1つ!(に Merge & Delete / Disolve した!)

f:id:USAGI-WRP:20200413205400p:plain

↑↓ Vertex Group は2つ!(Armatureとの対応も一部が壊れてしまっているのでうまく動かなくなっています)

f:id:USAGI-WRP:20200413205421p:plain

方法

Blender-2.82 時点では Vertex Group を直接操作して Merge する UI は無さそうなので、 Scripting を使います。今回は何かないかなーと探してみたところ:

-p2or/blender-merge-vertex-groups.py

↑いい感じのコード片が見つかったのでこれを応用します💪

手順:

  1. "いまは分かれてしまっているが統合したい" Vertex Group の正確な名前を確認します。 F2 で名前を編集状態にして Copy すると間違いが無くて安心
  2. ScriptingNew して p2or/blender-merge-vertex-groups.py を貼り付け
  3. 貼り付けた blender-merge-vertex-groups.py の group_input を ↑↑(1) で調べた名前群にする
  4. Object Mode で操作対象の Mesh が選択された状態にする
  5. Run Script
  6. 名前も "+" でくっついて統合された Vertex Group ができている(はず)なので確認
  7. 統合後の Vertex Group に対応付けたい Armature の名前を確認。 F2 で名前を編集状態にして Copy すると間違いが無くて安心
  8. 不要な場合は統合前の Vertex Group 群を - Vertex Groups UI の右側についているボタンをぽちって削除
  9. ↑↑↑(5) の統合された Vertex Group の名前を F2 で名前編集状態にして ↑↑(6) で確認した名前に変更。↑↑(6) で Copy してあれば Paste するだけ
  10. Pose Mode で期待動作を確認

Note: 統合後の Vertex Group の名前を統合前の Vertex Group で使用している名前にしたい場合は (7)と(8)を逆順で行います。

↓例えば Vertex Group 群 "NekoMimiBone_L_001""NekoMimiBone_L_003" を統合して "NekoMimiBone_L_001+NekoMimiBone_L_003" を作成し、

f:id:USAGI-WRP:20200413212459p:plain

f:id:USAGI-WRP:20200413213317p:plain

↓不要になった Vertex Group 群 "NekoMimiBone_L_001""NekoMimiBone_L_003" は Remove し、

f:id:USAGI-WRP:20200413213458p:plain

↓統合後の Vertex Group の名前を対応付けたい Armature と同じに

f:id:USAGI-WRP:20200413213810p:plain

します💪

f:id:USAGI-WRP:20200413214008p:plain

↑期待動作した成功✨

Blender Scripting: head と tail の座標と適当なフィルター条件でボーンの親子関係を自動構築する `connect-bones` の作り方と Add-on 化の方法

こんにちは、Python初心者∧Blender初心者です。フィーリングやマウスぽちぽちよりパキッとコードで処理する方が得意なので、 un-h002; Atroposcluster 対応で私自身に追加実装された Blener Scripting を入門講座を兼ねて整理します💪

Note: 読者の想定=PythonまたはBlenderは初心者Lv1くらいだけど、他に何かしらのプログラミング言語のLv25くらいの使い手の方(但しLv値はよくある1..99を基本想定とした古典的JRPGくらいの感覚値)で、必要な予備知識はAPI Referenceがあれば読みに行ける方。

準備するもの: bone が親子関係なしでたくさん入っていて、一部の bone は head と tail が同じ座標にあって親子関係で connected にできる Blender の作業状態。またはこのメモの学習用に Connected な親子関係のボーンを含んだデータを Blender ですべての armature を選択して親子関係を clear した作業状態。例えば セシル変身アプリ で作った VRM を前回のメモのように Blender へ Import するとそういう状態になるかもしれません。

もくじ

  1. はじまりの Blender Scripting
  2. すべての bone を列挙
  3. 列挙された bone 群をフィルター
  4. head と tail が同じ座標の bone 組の抽出
  5. 2つの bone 組の「選択」
  6. Blender 組み込み機能の Armature: Make Parent を呼ぶ
  7. かんせいの connect-bones Blender Scripting → Add-on 化

1. はじまりの Blender Scripting

  1. Blender上部のタブを Scripting へ切り替え、
  2. New Text して、
  3. 適当な名前を付けて
  4. 動作確認程度のコードを書いて
  5. Armature を Edit Mode で選択して (†後でここもコードにできます✨)
  6. Run Script

f:id:USAGI-WRP:20200412104746p:plain

import bpy # Blender API

print( bpy ) # You can output to STDOUT anything!
print( "Let's start", 123.45, 'Blender Scripting!' ) # You can concatenate a string and any object with a comma.
# In this world, you can use the `#` for a line commenting. `//` is not work.

Note: Blender-2.82@Windows 現在の Scripting のテキストエディターやSTDOUTの出力先の System Console Window は UNICODE 表示にデフォルトでは対応していないので非ASCIIをコードに使わないようにしました。

print による STDOUT の結果は F3 ->Wm: Toggle System Console` などすると表示されるシステムコンソール窓で確認できます。

<module 'bpy' from 'C:\\Program Files\\Blender Foundation\\Blender 2.82\\2.82\\scripts\\modules\\bpy\\__init__.py'>
Let's start 123.45 Blender Scripting!

こんな表示が Run Script で出れば Blender Scripting の Lv1 はクリアーです。おめでとうございます🎉

Lv1 をクリアーした報酬として BlenderAPI Documentation を進呈します:

今後の Blender Scripting ライフにお役立て下さい💪

2. すべての bone を列挙

bpy (たぶん Blender-PYthon とかそういう命名由来でしょう…) オブジェクトのメンバーを読んだり呼んだりすると、たいていの Blender の管理オブジェクトや組み込み機能の呼び出しを行えます。

例えば:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

for b in bones:
 print( b, ' { head=', b.head, ' tail=', b.tail, '}' )

こう↑して Run Script すると↓

<bpy_struct, Bone("NekoMimiBone_L")>  { head= <Vector (0.0699, 0.2111, 0.0399)>  tail= <Vector (0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_L_001")>  { head= <Vector (0.0825, 0.2277, 0.0401)>  tail= <Vector (0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_L_002")>  { head= <Vector (0.1012, 0.2548, 0.0416)>  tail= <Vector (0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_L_003")>  { head= <Vector (0.1210, 0.2814, 0.0430)>  tail= <Vector (0.1408, 0.3081, 0.0444)> }
<bpy_struct, Bone("Hair")>  { head= <Vector (-0.0000, 0.1213, 0.0039)>  tail= <Vector (-0.0000, 0.0988, 0.0286)> }
<bpy_struct, Bone("Hoho_R")>  { head= <Vector (-0.0817, 0.0463, 0.0867)>  tail= <Vector (-0.1030, 0.0584, 0.1093)> }
<bpy_struct, Bone("Hoho_L")>  { head= <Vector (0.0817, 0.0463, 0.0867)>  tail= <Vector (0.1030, 0.0584, 0.1093)> }
<bpy_struct, Bone("Ago")>  { head= <Vector (-0.0000, 0.0127, 0.0660)>  tail= <Vector (-0.0000, 0.0190, 0.0987)> }
<bpy_struct, Bone("Chest")>  { head= <Vector (-0.0000, -0.1039, 0.0311)>  tail= <Vector (0.0000, 0.0702, -0.0152)> }
<bpy_struct, Bone("Mune_R")>  { head= <Vector (-0.0940, -0.1429, 0.1315)>  tail= <Vector (-0.0521, -0.1654, 0.0071)> }
<bpy_struct, Bone("Mune_R_001")>  { head= <Vector (0.0000, 0.0000, 0.0000)>  tail= <Vector (-0.0000, 0.0333, -0.0000)> }
<bpy_struct, Bone("Mune_L")>  { head= <Vector (0.0940, -0.1429, 0.1315)>  tail= <Vector (0.0521, -0.1654, 0.0071)> }
<bpy_struct, Bone("Mune_L_001")>  { head= <Vector (0.0000, 0.0000, 0.0000)>  tail= <Vector (0.0000, 0.0333, -0.0000)> }
<bpy_struct, Bone("Koshi")>  { head= <Vector (-0.0000, 0.0000, 0.0000)>  tail= <Vector (-0.0000, 0.0100, 0.0000)> }

↑こんな具合の出力を得られます。

例を読んだ時点で既に本項の目的を達成していましましたね!🎉

素晴らしい学習能力を発揮中で知識を吸い込みたい盛りのあなたがLv2をクリアーした記念に:

この↑ Bone 型のリファレンスマニュアルを進呈します。 headtail が何かも書いてあるよ✨

3. 列挙された bone 群をフィルター

ところで、先程のコードに [ ] で囲まれたリストをコードで生成している部分がありました。あれが Python 言語の特徴的な言語機能の1つとして有名な "リスト内包表記" でした✨

リスト内包表記ではリストへ入れたい何かを、

  • 他のリストを for で列挙しつつ
  • if で条件を付けたり、
  • 取り出した結果そのものではなく加工を加えたオブジェクトにして

新たなリストを作成する、そんなような事ができる事を既に知りました。そこで、例えば:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

for b in filtered_bones:
 print( b, ' { head=', b.head, ' tail=', b.tail, '}' )

こんな↑具合で前項の例に1行 filtered_bones を "NekoMimiBone" で名前が始まる bone だけを抽出して作るように追加して、bones に替えて filtered_bonesprint してみると:

<bpy_struct, Bone("NekoMimiBone")>  { head= <Vector (-0.0000, 0.1213, 0.0039)>  tail= <Vector (-0.0000, 0.2111, 0.0399)> }
<bpy_struct, Bone("NekoMimiBone_R")>  { head= <Vector (-0.0699, 0.2111, 0.0399)>  tail= <Vector (-0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_R_001")>  { head= <Vector (-0.0825, 0.2277, 0.0401)>  tail= <Vector (-0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_R_002")>  { head= <Vector (-0.1012, 0.2548, 0.0416)>  tail= <Vector (-0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_R_003")>  { head= <Vector (-0.1210, 0.2814, 0.0430)>  tail= <Vector (-0.1408, 0.3081, 0.0444)> }
<bpy_struct, Bone("NekoMimiBone_L")>  { head= <Vector (0.0699, 0.2111, 0.0399)>  tail= <Vector (0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_L_001")>  { head= <Vector (0.0825, 0.2277, 0.0401)>  tail= <Vector (0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_L_002")>  { head= <Vector (0.1012, 0.2548, 0.0416)>  tail= <Vector (0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_L_003")>  { head= <Vector (0.1210, 0.2814, 0.0430)>  tail= <Vector (0.1408, 0.3081, 0.0444)> }

おめでとうございます🎉 今回も例を読んだだけで目的を達成してしまいましたね。Lv3のクリアーの記念に:

"Python-3.7" の公式チュートリアルの "§5.1.3. リスト内包表記" を進呈します💪 執筆現在の Python そのものは 3.8 が最新版ですが、 Blender-2.82 に組み込まれた Python は 3.7.4 です。リファレンスを参照する時は自分が扱う言語処理系のバージョンにも注意しましょう。

Note: ちなみに、ぐぐるとリスト内包表記は何なのか程度の"入門情報"はたくさんの野良解説が出てくると思います。Pythonの言語の入門書籍でも紹介されているでしょう。"Must"や"Should"と言うほどお節介ではありませんが、たいていのプログラミング言語の"基礎的な何か"は言語公式の資料を第一に参照すると書き間違いも滅多にありませんし、必要な事は必ず書いてありますから合理的でいいですよ👍

4. head と tail が同じ座標の bone 組の抽出

少し複雑さが増加しそうなので、リスト内包表記やワンライナーを無理に作ろうとせず、そろそろ関数を定義して数年後の自分でも見た瞬間に読めるように書きます:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'Parent:', p[0], ' <==/connectable/==< Child:', p[1] )

map は言語組み込みの元になるリストの要素群を任意のファンクターで射影変換した要素群から作成したリストを得る Python 言語のリスト処理用に組み込まれた関数です。また、 Python では [ ] はリスト、 ( ) はタプルになります。💪

Note: Python では def で関数を定義できます。いままで触れていませんでしたが、 Python の「らしい」言語機能のもう1つの大きな特徴のインデント(indent)/デデント(dedent)によるブロック構造の生成を前項までの最後の forprint の組み合わせでも書いてありました。 defif も同様に : の後の行で indent = { から dedent = } まで同じインデントレベルの連続した複数の行が1つのブロックとして処理されます。また、 Python では関数の return が未定義の場合は None が還ります。

↑これを実行すると↓

Parent: <bpy_struct, Bone("NekoMimiBone_R")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_001")>
Parent: <bpy_struct, Bone("NekoMimiBone_R_001")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_002")>
Parent: <bpy_struct, Bone("NekoMimiBone_R_002")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_003")>
Parent: <bpy_struct, Bone("NekoMimiBone_L")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_001")>
Parent: <bpy_struct, Bone("NekoMimiBone_L_001")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_002")>
Parent: <bpy_struct, Bone("NekoMimiBone_L_002")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_003")>

今回も簡単に目的を達成できました✨ 余力がみなぎっていかもしれないので、こちら↓をLv4のクリアーの記念に進呈します。ご活用下さい:

↑は "Python-3.7" 言語リファレンスの "§2.1.8. インデント" です💪 言語リファレンスはチュートリアルよりもカチッと規格の解説らしい文書なので、じっくり整理しながら読む必要のある部分も少なくありませんが、言語機能の中枢を成す基礎的な部分、例えばインデントとデデントによるコードブロックの生成の正確な情報、空白文字なら何でも良いのか、タブとスペースが混ざっているとどうなるのか、そういった情報について言語規格に基づいた間違いの(たいていは)無い正確な情報を確認できます。言語機能の基礎的な要素について気になる事があれば、はじめに言語リファレンスを参照すると不安の無い確実な情報が(たぶん)取得できて便利です✨

5. 2つの bone 組の「選択」

ここまでは Blender API を通して Blender が管理中のオブジェクトについて read 的なアクセスでした。そろそろ write 的な事をしてみましょう。世界は"副作用"で"進んで"います:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'Parent:', p[0], ' <==/connectable/==< Child:', p[1] )
 p[0].select = True
 p[1].select = True

↑最後の2行が増えただけですが、これまでにない大きな違いがあります。このコードでは最後の2行でスクリプトの外の Blender の世界の値を書き換えています。実行後に Edit Mode で今回操作対象のネコミミのあたりを見ると:

f:id:USAGI-WRP:20200412104948p:plain

ネコミミ部分の Connected な親子関係を見るからに構築できそうなボーン群が選択状態になりましたね!このスクリプトは実行によりスクリプトの外の Blender の世界に副作用を引き起こす事に成功しました。おめでとうございます🎉

ちなみに↑で EditBoneselect プロパティーの仕様を確認できます✨ Blender API には多くの "操作" が bpy.ops にメソッドとして提供されていて、例えば bpy.ops.armature.select_all のような使い所次第では便利な機能もあります。一方で、今回使った EditBoneselect プロパティーのように、直接変数の値を書き換えればよい場合もあります💪

6. Blender 組み込み機能の Armature: Make Parent を呼ぶ

既に bpy.ops.armature.parent_set を呼ぶだけでしょ、と勘付いているかもしれません。その通りです💪 その通りではあるのですが、きちんと API Reference を見てから使わないと使えません。

ついで、そろそろ仕上げも近づいているので、スクリプトの実行前に手作業で Edit Mode へ切り替えておく必要も無いように bpy.ops.object.mode_set も組み込んでしましょう:

import bpy
from itertools import chain

bpy.ops.object.mode_set(mode='EDIT')

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'connect-bones:', p[0].name, '<-', p[1].name )
 p[1].select = True
 p[0].select = True
 armatures[0].edit_bones.active = p[0]
 bpy.ops.armature.parent_set()
 p[0].select = False
 p[1].select = False

for p in connectable_pairs:
 p[1].select = True
 p[0].select = True

GUIで操作する場合には「先っぽボーン」→「根本ボーン」の順序で「選択」して Armature: Make Parent すれば期待動作するこの機能ですが、 bpy.ops.armature.parent_setスクリプトで使う場合は「選択」だけでなく「アクティブ状態」も意識的に操作を記述する必要があります。GUI操作では最後にぽちっと選択したボーンが「選択状態」かつ「アクティブ状態」になりますが、スクリプトでは「選択」と「アクティブ化」のコードはそれぞれ別に記述する機能として API が提供されています💪

加えて、GUI操作と同様の点として、複数の親子関係の構築をする場合には、先に親子関係を構築したボーンは選択を解除して、それから次の親子関係の選択を行う必要があります。

なお、今回は bpy.ops.object.mode_set(mode='EDIT') も加えたので、例えば Pose ModeRun Script しても自動的に Edit Mode になり、スクリプトが期待動作するようになっています💪

さらに、おまけ機能として親子関係の構築をすべて行った後、もう一度、親子関係のタプルをトラバースして親子関係を構築した(はず)のボーン、つまり変更があったボーン群を「選択」した状態にしています。システムコンソールのログを読まなくてもGUIで視覚的にどこがくっついたのかわかりやすくする工夫です。

f:id:USAGI-WRP:20200412105016p:plain

実行すると "NekoMimiBone" で始まるボーン群について、いまは互いにばらばらで親子関係は無いけれど tail と head が同じ座標で重なっている bone の組をすべて見つけて Connected な親子関係を構築してくれます✨ Connected なので、繋がったボーンの1つを適当に選択して G で動かすなどすると繋がったボーンも影響を受けて回転したり拡縮したり追従するようになっています:

f:id:USAGI-WRP:20200412105547p:plain

7. かんせいの connect-bones Blender Scripting -> Add-on 化✨

↑までで目的を達成するスクリプトを手書きで実行できるようになりました✨ せっかくなので Add-on にしてみます:

チュートリアルを参考にもにょもにょ書いて試して修正して:

### The code for add-on ecosystem ###

bl_info = {
 'name'    : 'connect-bones',
 'blender' : ( 2, 80, 0 ),
 'category': 'Armature',
 'author'  : 'USAGI.NETWORK / Usagi Ito',
 'version' : ( 0, 0, 0 ),
 'support' : 'TESTING',
}

addon_keymaps = []

import bpy
from itertools import chain
from re import compile

class ConnectBones( bpy.types.Operator ):
 '''Connect bones if it find a bone pair that has a same position of a head and a tail'''
 bl_idname  = 'armature.connect_bones'
 bl_label   = 'Connect bones'
 bl_options = { 'REGISTER', 'UNDO' }
 
 name_regex: bpy.props.StringProperty( name = 'name_regex', default = '.*' )
 
 def execute( self, context ):
  return connect_bones( self.name_regex )

def register():
 bpy.utils.register_class( ConnectBones )
 bpy.types.VIEW3D_MT_object.append( menu )
 wm = bpy.context.window_manager
 kc = wm.keyconfigs.addon
 if kc:
  km = wm.keyconfigs.addon.keymaps.new( name = '3D View', space_type = 'VIEW_3D' )
  kmi = km.keymap_items.new( idname = ConnectBones.bl_idname, type = 'C', value = 'PRESS', ctrl = True, shift = True, alt = True )
  kmi.properties.name_regex = '(?!)'
  addon_keymaps.append( ( km, kmi ) )

def unregister():
 bpy.utils.unregister_class( ConnectBones )
 for km, kmi in addon_keymaps:
  km.keymap_items.remove(kmi)

def menu( self, context ):
 self.layout.separator()
 self.layout.operator( ConnectBones.bl_idname )
 addon_keymaps.clear()

if __name__ == '__main__':
 register()

### The main code of the add-on ###

def connect_bones( name_regex ):
 bpy.ops.object.mode_set(mode='EDIT')
 
 armatures = bpy.data.armatures
 bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )
 
 regex = compile( name_regex )
 
 filtered_bones = [ b for b in bones if regex.match( b.name ) ]
 
 def find_connectable_pair( parent ):
  for candidate in filtered_bones:
   if parent.tail == candidate.head:
    return ( parent, candidate )
 
 connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]
 
 for p in connectable_pairs:
  print( 'connect-bones:', p[0].name, '<-', p[1].name )
  p[1].select = True
  p[0].select = True
  armatures[0].edit_bones.active = p[0]
  bpy.ops.armature.parent_set()
  p[0].select = False
  p[1].select = False
 
 for p in connectable_pairs:
  p[0].select = True
  p[1].select = True
 
 return {'FINISHED'}

こんな具合でできました✨

add-on 化にあたり、

  1. add-on として機能を汎用的にするため前項までの試作では startwith でフィルターしていた bone の名前を正規表現でフィルターする方式に変更
  2. CTRL + SHIFT + ALT + C ショートカットキーで正規表現のパターンを指定するダイアログ付きで発動可能
  3. Python Console を使う場合は bpy.ops.armature.connect_bones( name_regex= 'Neko' ) のように発動可能

にしました💪

f:id:USAGI-WRP:20200412110456p:plain

f:id:USAGI-WRP:20200412110450p:plain

この add-on を使うと、名前を正規表現で狙ったボーン群に Connected な親子関係を構築できるので、例えばネコミミだけ、髪の毛だけなどある程度簡単に狙って Connected な親子関係をばらばらのボーンから構築できます。こうして Connected な親子関係を作ると Blender の内臓機能で Connected な親子ボーン群は任意に Armature: Merge Bones でき、そうするとマージ元のボーンそれぞれに割り当てられていた頂点ブレンディングのウェイトを合成して引き継いだボーンと頂点ウェイトの関係が自動的にできるので便利、という add-on です。

f:id:USAGI-WRP:20200412110246p:plain

↑見えるボーンのほとんどが Connected な親子関係にできるデータですが、手で作業するには多すぎて多すぎます…。でも、この add-on があれば↓

f:id:USAGI-WRP:20200412110255p:plain

めでたしめでたし。おしまい💪