Electron アプリに electron-store で設定ファイル機能を追加する場合の example & tips 的なメモ
yarn add electron-store
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:
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のアクセラレーターも使わないキーバインドを追加するメモ
準備:
yarn add electron-localshortcut
; https://www.npmjs.com/package/electron-localshortcut
実装例(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のアクセラレーター機能(メニューに項目を作る必要が生じる)、レンダラープロセス側でにゃんにゃん(若干面倒&今回ケースの用途では直接レンダラープロセス側にコードをごりごりするのは微妙に美しくない気がしたので今回は↑の方法へ)
- Note: 公式情報として参考にはなるけれど
Electron アプリに i18next を追加して i18n/l10n 対応するメモ
基本的な使用の流れ
yarn add i18next i18next-node-fs-backend
public/i18n.js
など適当に作り- l10n 文字列を取り出したいソースで
const i18n = require('./i18n.js')
して - 必要なら
await i18n.changeLanguage('ja-JP')
のように l10n 先を実行時に設定する仕込みをして i18n.t('hoge')
とすると i18next で l10n した文字列を取り出せます- i18next の options で渡す
loadPath
に JSON で{ "hoge": "ほげ" }
のように key-value 的に l10n リソースを定義追加します。saveMissing
をtrue
にしておけばaddPath
で与える missing 用のファイルへ l10n リソースが未定義で実行時に要求された文字列のリストが保存されます。
public/i18n.js
- Note
- ファイル名や配置はお好みで。
options
は Configuration 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
を編集します。
心穏やかに闇のInkscapeを使えるようになります👍
参考
- Inkscape - Permanently Change Default Document Background Color - Graphic Design Stack Exchange
- Note: ここで紹介されている方法のパスは実際には少なくとも Inkscape-1.0 では若干ですが異なります。
Blender: 正規表現でがさっと Vertex Group(s) をタゲって統合しつつ元は削除して置き換えられる Scripting: usagi/blender-merge-vertex-groups.py のメモ
前回のメモで使用した p2or/blender-merge-vertex-groups.py を元に:
- 正規表現
regex
設定で Vertex Group(s) をまとめてタゲれて - 統合元になった Vertex Group(s) は削除する replace モードを
True
/False
設定できて - 統合先を明示的な名付け
to
またはそれが未設定なら"+".join
できて - 削除と作成について簡単なログを吐いてくれる
dry
をTrue
/False
して dry-run (実際には操作しないけど今の設定で実行するとこれこれが remove でこんな merge ができるよ、を表示だけ)してくれる (2020-04-14追記)
ように改造した Fork:
を作りました💪 便利ですよ✨
連続で複数パターンを操作する場合は add-on にするならちゃんとGUIを付けないと使いにくそうだし、 Scripting で regex
, replace
, to
を逐次書き換えながら使ってね、でもとりあえずの便利には十分かなって思います。
(2020-04-14追記:↓dry-run)
Blender: Armature の削除や変更で Vertex Group との整合性が狂ってしまったときの修正方法、のメモ
状況
「Armature を節約しなきゃ→簡単に Merge できるところは Merge!→ Merge が難しいところは Delete / Disolve ! 」
とかした後に、
「 Armature を Delete / Disolve した部分に対応していた Vertex Group と残した Armature との対応が壊れてしまった…再ペイントは…しんどいでござる」
になった時、 Weight Paint で塗り絵せずに残っている Vertex Group をにゃんにゃんしてうまいことする方法のメモです。
例えば、
↑ Armature は1つ!(に Merge & Delete / Disolve した!)
↑↓ Vertex Group は2つ!(Armatureとの対応も一部が壊れてしまっているのでうまく動かなくなっています)
方法
Blender-2.82 時点では Vertex Group を直接操作して Merge する UI は無さそうなので、 Scripting を使います。今回は何かないかなーと探してみたところ:
-p2or/blender-merge-vertex-groups.py
↑いい感じのコード片が見つかったのでこれを応用します💪
手順:
- "いまは分かれてしまっているが統合したい"
Vertex Group
の正確な名前を確認します。F2
で名前を編集状態にしてCopy
すると間違いが無くて安心 Scripting
でNew
して p2or/blender-merge-vertex-groups.py を貼り付け- 貼り付けた blender-merge-vertex-groups.py の
group_input
を ↑↑(1) で調べた名前群にする Object Mode
で操作対象の Mesh が選択された状態にするRun Script
- 名前も
"+"
でくっついて統合されたVertex Group
ができている(はず)なので確認 - 統合後の Vertex Group に対応付けたい Armature の名前を確認。
F2
で名前を編集状態にしてCopy
すると間違いが無くて安心 - 不要な場合は統合前の Vertex Group 群を
-
Vertex Groups UI の右側についているボタンをぽちって削除 - ↑↑↑(5) の統合された Vertex Group の名前を
F2
で名前編集状態にして ↑↑(6) で確認した名前に変更。↑↑(6) でCopy
してあればPaste
するだけ - Pose Mode で期待動作を確認
Note: 統合後の Vertex Group の名前を統合前の Vertex Group で使用している名前にしたい場合は (7)と(8)を逆順で行います。
↓例えば Vertex Group 群 "NekoMimiBone_L_001"
と "NekoMimiBone_L_003"
を統合して "NekoMimiBone_L_001+NekoMimiBone_L_003"
を作成し、
↓不要になった Vertex Group 群 "NekoMimiBone_L_001"
と "NekoMimiBone_L_003"
は Remove し、
↓統合後の Vertex Group の名前を対応付けたい Armature と同じに
します💪
↑期待動作した成功✨
Blender Scripting: head と tail の座標と適当なフィルター条件でボーンの親子関係を自動構築する `connect-bones` の作り方と Add-on 化の方法
こんにちは、Python初心者∧Blender初心者です。フィーリングやマウスぽちぽちよりパキッとコードで処理する方が得意なので、 un-h002; Atropos の cluster 対応で私自身に追加実装された 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 するとそういう状態になるかもしれません。
もくじ
- はじまりの Blender Scripting
- すべての bone を列挙
- 列挙された bone 群をフィルター
- head と tail が同じ座標の bone 組の抽出
- 2つの bone 組の「選択」
- Blender 組み込み機能の
Armature: Make Parent
を呼ぶ - かんせいの
connect-bones
Blender Scripting → Add-on 化
1. はじまりの Blender Scripting
- Blender上部のタブを
Scripting
へ切り替え、 New Text
して、- 適当な名前を付けて
- 動作確認程度のコードを書いて
- Armature を Edit Mode で選択して (†後でここもコードにできます✨)
Run Script
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 をクリアーした報酬として Blender の API 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
型のリファレンスマニュアルを進呈します。 head
や tail
が何かも書いてあるよ✨
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_bones
を print
してみると:
<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)によるブロック構造の生成を前項までの最後の for
と print
の組み合わせでも書いてありました。 def
や if
も同様に :
の後の行で 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 で今回操作対象のネコミミのあたりを見ると:
ネコミミ部分の Connected な親子関係を見るからに構築できそうなボーン群が選択状態になりましたね!このスクリプトは実行によりスクリプトの外の Blender の世界に副作用を引き起こす事に成功しました。おめでとうございます🎉
ちなみに↑で EditBone
の select
プロパティーの仕様を確認できます✨ Blender API には多くの "操作" が bpy.ops
にメソッドとして提供されていて、例えば bpy.ops.armature.select_all
のような使い所次第では便利な機能もあります。一方で、今回使った EditBone
の select
プロパティーのように、直接変数の値を書き換えればよい場合もあります💪
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 Mode
で Run Script
しても自動的に Edit Mode
になり、スクリプトが期待動作するようになっています💪
さらに、おまけ機能として親子関係の構築をすべて行った後、もう一度、親子関係のタプルをトラバースして親子関係を構築した(はず)のボーン、つまり変更があったボーン群を「選択」した状態にしています。システムコンソールのログを読まなくてもGUIで視覚的にどこがくっついたのかわかりやすくする工夫です。
実行すると "NekoMimiBone" で始まるボーン群について、いまは互いにばらばらで親子関係は無いけれど tail と head が同じ座標で重なっている bone の組をすべて見つけて Connected な親子関係を構築してくれます✨ Connected なので、繋がったボーンの1つを適当に選択して G
で動かすなどすると繋がったボーンも影響を受けて回転したり拡縮したり追従するようになっています:
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'}
こんな具合でできました✨
- https://github.com/usagi/blender-connect-bones (↑の初版より進化している可能性があります)
add-on 化にあたり、
- add-on として機能を汎用的にするため前項までの試作では
startwith
でフィルターしていた bone の名前を正規表現でフィルターする方式に変更 CTRL + SHIFT + ALT + C
ショートカットキーで正規表現のパターンを指定するダイアログ付きで発動可能Python Console
を使う場合はbpy.ops.armature.connect_bones( name_regex= 'Neko' )
のように発動可能
にしました💪
この add-on を使うと、名前を正規表現で狙ったボーン群に Connected な親子関係を構築できるので、例えばネコミミだけ、髪の毛だけなどある程度簡単に狙って Connected な親子関係をばらばらのボーンから構築できます。こうして Connected な親子関係を作ると Blender の内臓機能で Connected な親子ボーン群は任意に Armature: Merge Bones
でき、そうするとマージ元のボーンそれぞれに割り当てられていた頂点ブレンディングのウェイトを合成して引き継いだボーンと頂点ウェイトの関係が自動的にできるので便利、という add-on です。
↑見えるボーンのほとんどが Connected な親子関係にできるデータですが、手で作業するには多すぎて多すぎます…。でも、この add-on があれば↓
めでたしめでたし。おしまい💪