- アニメーションGIF出力機能追加(透過対応、gif_encoder.js 新規) - GIFカラー量子化・透過インデックスバグ修正 - PSW追加/削除後のレイヤータブ即時更新 - PスイッチタブのスロットボタンUI改善(+MLP黄・+SLP水色・順序変更) - レイヤータブ「SW追加/削除」→「LSW追加/削除」リネーム - docs: 0_node.png・1_node.png更新、8_pose_switch.png追加 - user_data: LSW+PSW設定済みモデル・PSWプロジェクト追加 - README(3言語)・DEVLOG更新 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1930 lines
122 KiB
Markdown
1930 lines
122 KiB
Markdown
# Development Log — ComfyUI PSD Figure Creator
|
||
|
||
---
|
||
|
||
## v0.6.8 — 2026-06-24
|
||
|
||
### Changed
|
||
- **PスイッチタブのスロットボタンUI改善** — ボタンの並び順を変更し、ポーズ登録系ボタンを左に集約:
|
||
- 順序: `+MLP`(黄)→ `+SLP`(水色)→ `−LP` → スペース → `+Slot` → `−Slot`
|
||
- `+MLP` に黄色、`+SLP` に水色のテーマカラーを適用し、0° スロット操作時に使用するボタンが視覚的に区別しやすくなった
|
||
|
||
---
|
||
|
||
## v0.6.7 — 2026-06-24
|
||
|
||
### Added
|
||
- **アニメーションGIF出力** — キーフレームパネルのWebMボタン右隣に「🎞️ GIF」ボタンを追加。透過対応アニメーションGIFをエクスポート。`web/js/gif_encoder.js` を新規作成(NeuQuant カラー量子化 + GIF LZW エンコーダー + GIF89a フォーマット出力)
|
||
- **ヘルプにキーフレームアニメーションセクション追加** — 全3言語(日本語・英語・中国語)でキーフレームパネルの全ボタンの説明を追加
|
||
|
||
### Fixed
|
||
- **GIF カラー量子化スケールバグ** — NeuQuant 学習時にRGBピクセル値にネットワークスケール(`<< netbiasshift`)を適用していなかったため、パレットが黒に近い値に収束しシルエット状になる問題を修正
|
||
- **GIF 透過インデックスオーバーフロー** — `map()` が255を返したとき `+1=256` が `Uint8Array` で0(透過インデックス)に切り捨てられ、白やパレット末尾の色が透過になる問題を `Math.min(254, ...)` で修正
|
||
- **GIF アルファ処理改善** — 透過閾値を `alpha < 128` から `alpha < 32` に引き下げ、`32 ≦ alpha < 255` の半透明ピクセルにアルファ合成(プレマルチプライ)を適用してフチの欠落を低減
|
||
- **PSW追加/削除後のレイヤータブ表示更新漏れ** — `_createPswLayer()` / `_deletePswLayer()` に `_renderTree()` 呼び出しを追加し、LSWと同様にボタン操作直後にレイヤーリストが更新されるよう修正
|
||
|
||
### Changed
|
||
- **レイヤータブ ボタン名変更** — 「SW追加」「SW削除」→「LSW追加」「LSW削除」に変更(i18n.js 全3言語)
|
||
- **ヘルプのLSWボタン名修正** — ヘルプ内の「SW追加/削除」表記を「LSW追加/削除」に統一(全3言語)
|
||
|
||
---
|
||
|
||
## v0.6.6 — 2026-06-23
|
||
|
||
### Fixed
|
||
- **PSW ON/OFF 状態の保存・復元を全箇所に対応**:
|
||
- **ポーズ保存(通常・SW付き)**: `pswEnabled` を保存データに追加。ロード時に `node._pswEnabled` を復元しボタン色を更新
|
||
- **モデル保存**: `configObj.pswEnabled` を追加
|
||
- **モデルロード**: `data.layer_config.pswEnabled` を `node._pswEnabled` に復元
|
||
- **確定ボタン(onApplyConfig)**: 既存 `layer_config` の `pswEnabled` を新 `configObj` に引き継ぎ
|
||
- **キーフレーム**: 各フレームに `pswEnabled` を保存・ステップ補間で復元(前バージョンより)
|
||
- **プロジェクト(layer_config)**: トグル操作時に `layer_config` へ書き込み、`onConfigure` で復元(前バージョンより)
|
||
|
||
---
|
||
|
||
## v0.6.5 — 2026-06-23
|
||
|
||
### Added
|
||
- **PSW ON/OFF 状態をキーフレームに保存** — キーフレーム登録時に `pswEnabled` フラグを保存し、シーク・再生時に復元
|
||
- PSW トグルの ON/OFF がキーフレームごとに切り替わるアニメーションが可能
|
||
- `pswEnabled` はステップ補間(直前のキーフレームの値を次のキーフレームまで保持)
|
||
- `node._updatePswToggle` を外部参照可能にし、シーク時にボタンの青/赤表示を即時更新
|
||
|
||
---
|
||
|
||
## v0.6.4 — 2026-06-23
|
||
|
||
### Added
|
||
- **PSW ON/OFF トグルボタン(ノード)** — ノードの Capture ボタン左に `PSW` トグルボタンを追加:
|
||
- **青(ON)**: PSW プリセットポーズを有効化(デフォルト)
|
||
- **赤(OFF)**: PSW を無効化。PSW 登録レイヤーも含めて全レイヤーを R/MR で自由操作可能
|
||
- モーダルプレビューにも即時反映(`this.node._pswEnabled` 参照)
|
||
- **ヘルプ(3言語)更新** — PSwitch タブに「PSW トグル」の説明行を追加
|
||
|
||
---
|
||
|
||
## v0.6.3 — 2026-06-23
|
||
|
||
### Changed
|
||
- **PSWポーズ登録ワークフロー刷新**:
|
||
- **+MLP / +SLP は 0° スロット専用** — 0° 以外のスロットでは何もしない。0° スロットで登録するレイヤー(群)を確定するための操作として明確化
|
||
- **0° 以外のスロットは ✏編集 → ✓確定 フローで保存** — `✏編集` でスロットの登録済みポーズ(未登録時は 0° スロットのポーズ)を `state.pose` にロード、ポーズ調整後に `✓確定` で 0° スロットのレイヤー群に現在のポーズを書き戻す
|
||
- **+Slot / −Slot ボタン名** — PSW スロット追加/削除ボタンを `+` / `−` から `+Slot` / `−Slot` にリネーム
|
||
- **PSW 可動域ロック修正** — `(slots.length - 1) × PSW_STEP` を最大角度に変更。1スロット=0°固定、2スロット=0°〜30°、3スロット=0°〜60°
|
||
- **セットアップモードのガイドアーク修正** — アーク描画範囲を `PSW_STEP × n`(旧: スロット数+1分)から `PSW_STEP × (n-1)`(最終スロットまで)に修正
|
||
- **ヘルプダイアログ(3言語)更新** — PSW セクションを新ワークフローに合わせて全面改訂。+Slot / −Slot、+MLP/+SLP の 0° 専用化、✏編集→✓確定 の手順を明記
|
||
|
||
### Fixed
|
||
- **+SLP のレイヤー未選択エラーが 0° 以外のスロットで出続ける** — `_lastSlpLayerId` が null のとき 0° スロットに登録済みのレイヤーを自動フォールバックとして使用するよう修正
|
||
- **スロット行クリック時に編集モードが終了しない** — `slotRow.onclick` で `_pswEditingPointId = null` を設定し、他スロット選択時に自動的に編集モードを終了
|
||
|
||
---
|
||
|
||
## v0.6.2 — 2026-06-23
|
||
|
||
### Added
|
||
- **+MLP / +SLP ポーズ登録の分割** — Pスイッチタブのポーズ登録ボタンを2種類に分割:
|
||
- **+MLP**(マルチレイヤーポーズ)— 従来の `+pset` と同じ動作。現在の全レイヤーポーズをスロットに一括登録
|
||
- **+SLP**(シングルレイヤーポーズ)— レイヤーツリーで選択中の1レイヤーのポーズのみをスロットにマージ登録。既存の他レイヤーポーズは保持される
|
||
- **−LP** — 従来の `-pset` をリネーム。スロットのポーズをクリア
|
||
|
||
- **+SLP レイヤー記憶** — `_lastSlpLayerId` で最後に使ったレイヤーIDを保持。0°スロットでレイヤーを選択して登録した後、30°・60°… のスロットではレイヤー再選択なしで同じレイヤーに +SLP 可能。レイヤーを選択し直すと上書き更新される
|
||
|
||
- **PSWハンドルの可動域ロック** — Pose モード・ノードキャンバス両方で、PSW ハンドルの回転を `[0°, 登録スロット数 × 30°]` の範囲にクランプ。3スロットなら0°〜90°、スロット追加で自動的に可動域が広がる。Setup モードでの初期配置ドラッグはクランプ対象外
|
||
|
||
### Changed
|
||
- **レイヤータブ: `[SW]` → `[LSW]`** — LSW レイヤーのプレフィックス表示を全3言語で変更 (`i18n.js`)
|
||
- **LSW / PSW 排他選択** — レイヤータブで LSW レイヤーを選択すると PSW の選択が解除され、PSW レイヤーを選択すると LSW・通常レイヤー選択が解除される
|
||
- **LSW / PSW カラーテーマ変更** — レイヤータブ・Lスイッチタブ・Pスイッチタブで色を統一:
|
||
- LSW: `#a78bfa`(紫)→ `#55aaff`(青)、ボーダー `#7c3aed` → `#2266cc`
|
||
- PSW: `#cc88ff`(薄紫)→ `#ff7766`(赤)、ボーダー `#9944cc` → `#cc3344`
|
||
|
||
### UX
|
||
- **PSWスロット表示の改善**:
|
||
- 未登録: `未登録` → `−`
|
||
- SLP登録(1レイヤー): `✓ 登録済み (1レイヤー)` → `✓ : レイヤー名`(`state.renamed` 優先、なければ PSD 元名)
|
||
- MLP登録(複数レイヤー): `✓ 登録済み (Nレイヤー)` → `✓ (Nレイヤー)`
|
||
|
||
---
|
||
|
||
## v0.6.1 — 2026-06-22
|
||
|
||
### Added
|
||
- **PSWキーフレーム対応** — キーフレームシステムが PSW ハンドル角度(`psw_angles`)を記録・補間・復元するように対応:
|
||
- `addKeyframeAtCurrentFrame`: `psw_layers` の各ポイント angle を `psw_angles` としてキーフレームに保存
|
||
- `_kfGetInterpolatedState`: `psw_angles` をフレーム間で最短回転パス補間
|
||
- `seekToFrame`: 補間済み `psw_angles` を `config.psw_layers` の各ポイントに反映
|
||
|
||
### Fixed
|
||
- **PSWレイヤー名変更ができない** — `_renderPswTab` でクリックハンドラが即座に `_renderPswTab()` を呼ぶためダブルクリックイベントが届かなかった。クリック処理を 260ms タイマー遅延に変更し、レイヤー名を `<span>` 要素として保持することでインライン編集を有効化
|
||
- **レイヤータブで PSW レイヤー名を変更できない** — `_mkPswLayerEl` に dblclick リネームを追加(SW レイヤーと同パターン)
|
||
- **ポーズ保存(右クリック)に PSW ハンドル角度が含まれない** — モーダルの `_savePoseWithSwFromModal` とノードのポーズスナップ右クリック両方で `psw_angles` を収集・保存するよう修正
|
||
- **ポーズロード時に PSW ハンドル角度が適用されない** — `_loadPose` の通常ポーズ復元ブロックに `psw_angles` → `psw_layers[].points[].angle` への反映処理を追加
|
||
|
||
### Changed
|
||
- **ヘルプダイアログ** — 3言語すべて更新:
|
||
- レイヤータブ: PSW レイヤー名変更の説明を追加
|
||
- Pose モード: 「ポーズ保存 右クリック」の説明を "SW 状態込み" → "LSW/PSW ハンドル状態込み" に修正
|
||
|
||
### Docs
|
||
- **README 3言語** — キーフレーム補間表に `PSW ハンドル角度` 行を追加、`layer_config` スキーマの `keyframes` エントリに `psw_angles` フィールドを追加
|
||
- **README 3言語** — トラブルシューティングセクションを新設: psd-tools が出力する `Unknown image resource` / `Unknown tagged block` の INFO メッセージは無害であることを説明
|
||
|
||
---
|
||
|
||
## v0.6.0 — 2026-06-22
|
||
|
||
### Overview
|
||
スイッチ(SW)機能をレイヤースイッチ(LSW)とポーズスイッチ(PSW)に分割。LSW は従来の「ハンドル角度でグループ表示を切り替える」機能をそのまま継承。新規追加の PSW はハンドル角度に応じて登録済みポーズ(R/MR の状態)を自動適用するスイッチで、30°刻みのスロットにポーズを登録し、最大12スロットまで対応。
|
||
|
||
### Added
|
||
- **PSW(ポーズスイッチ)ポイント** — 白いオリジン + 紫のハンドル。ハンドルを回転させて登録済みポーズスロットを切り替える:
|
||
- **配置**: Setupモードで PSW ボタン(初回押下時にPSWレイヤーを自動作成)→ キャンバスをクリック
|
||
- **スロット管理**: `+` で30°刻みのスロットを追加(最大12スロット)、`−` で削除
|
||
- **ポーズ登録**: Poseモードで姿勢を決めてスロットを選択 → `+pset` で現在のポーズを登録、`−pset` でクリア
|
||
- **ポーズ適用**: ハンドル角度が 0°〜30°→スロット0、30°〜60°→スロット1… と切り替わる
|
||
- 複数PSWポイントは独立して動作し、それぞれのポーズが合成される
|
||
|
||
- **`computePswEffectivePose(basePose, pswLayers, editingPswPointId)`** — PSWポイントの角度からアクティブスロットを特定し、ベースポーズに登録ポーズを合成して返す。モーダルプレビューとノードキャンバス両方で使用。`editingPswPointId` を指定するとそのポイントのオーバーライドを無効化(`+pset` で編集中にR/MRの変化をリアルタイム確認可能)
|
||
|
||
- **Pスイッチ(PSW)タブ** — セットアップモーダルに4番目のタブとして追加:
|
||
- 上段: PSWレイヤー/ポイントリスト(選択・リネーム・削除ボタン付き)
|
||
- 中段: スロットリスト(登録済み/未登録の状態表示、クリックでアクティブスロット変更・ハンドルが対応角度に移動)
|
||
- 下段: `+` / `−` / `+pset` / `−pset` の4ボタン
|
||
|
||
- **PSWボタン** — Setup バーに「PSW」ボタンを追加(紫系テーマ)。押下時にPSWレイヤーが存在しなければ自動作成し即座にポイント配置可能
|
||
|
||
- **ノードキャンバス対応** — ノードキャンバスでもPSWポイントのヒットテスト・ハンドルドラッグ・ポーズ適用に対応
|
||
|
||
- **i18n** — PSW関連翻訳キーを ja / en / zh に追加: `tabPSwitch`, `addPswLayer`, `deletePswLayer`, `deletePswPointTooltip`, `addPswSlotTooltip`, `deletePswSlotTooltip`, `setPswPoseTooltip`, `clearPswPoseTooltip`, `maxPswSlotsReached`
|
||
|
||
### Changed
|
||
- **スイッチタブ名変更** — "スイッチ" → "Lスイッチ" (ja) / "LSwitch" (en) / "L切换" (zh)
|
||
- **SWボタン** — セットアップバーの "SW" ボタンを "LSW" にリネーム
|
||
- **ヘルプダイアログ** — 3言語すべて更新: スイッチ→Lスイッチ(LSW)にリネーム、Pスイッチ(PSW)セクション新規追加、Setup/Poseモードに LSW/PSW の説明を追加
|
||
|
||
### Fixed
|
||
- **PSWボタン押下後にポイントを配置できない** — ボタンはモードを `psw` に変更するだけで `_selectedPswLayerId` が未設定のままキャンバスクリックがパンになっていた。PSWボタン onclick に「レイヤーが無ければ自動作成、あれば先頭を選択」ロジックを追加
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
// layer_config に追加されるPSWフィールド
|
||
{
|
||
"psw_layers": [{
|
||
"id": "pswl_xxx",
|
||
"name": "PSW1",
|
||
"points": [{
|
||
"id": "pswp_xxx",
|
||
"name": "PSW1",
|
||
"x": 512,
|
||
"y": 512,
|
||
"radius": 80,
|
||
"angle": 0,
|
||
"slots": [
|
||
{ "degree": 0, "pose": null },
|
||
{ "degree": 30, "pose": { "<layerId>": { "angle": 0.5, "tx": 10, "ty": -5 } } }
|
||
]
|
||
}]
|
||
}]
|
||
}
|
||
```
|
||
`psw_layers` がない既存設定は従来通り動作(後方互換)。
|
||
|
||
---
|
||
|
||
## v0.5.2 — 2026-06-19
|
||
|
||
### Fixed
|
||
- **別PC移行時に Setup が開けない** (`openLayerModal`) — ワークフローを別PCに持ち込んだ場合など、`psd_filename` ウィジェットに値があるが PSD ファイルが `input/psd/` に存在しないとき、「レイヤー情報の取得に失敗しました」アラートを表示した後 `return` でモーダル表示をブロックしていた。`return` を削除し、PSD が見つからなくてもモーダルを空レイヤー状態で開くように修正。モーダルが開けば「Open PSD」ボタンから PSD を再選択・アップロードできる。
|
||
|
||
---
|
||
|
||
## v0.5.1 — 2026-06-18
|
||
|
||
### Added
|
||
- **キーフレーム移動トグル (`↔`)** — Row A に `↔` ボタン(`kfMoveKeyBtn`)を追加。ON 状態でタイムラインのドラッグがキーフレームを移動する(プレイヘッドは無効化)。OFF 状態は従来通りプレイヘッドをドラッグ。ON 時はボタンが紫系ハイライト(`background:#3a2a4a; border:#8a5a9a; outline:1px solid #aa77cc`)で状態を示し、ダイヤモンドマーカー 12px 以内でカーソルが `grab` になる。ポーズのみ・カメラのみ・両方を含むどの KF もまとめて移動する(`frame` フィールドを書き換えて `layer_config.keyframes` を即時更新)。移動先に既存 KF がある場合は上書き
|
||
|
||
- **新関数 `moveKeyframe(node, fromFrame, toFrame)`** — 指定フレームの KF エントリ全体を別フレームへ移動。移動先に既存 KF があれば先に削除してから追加しソート。`layer_config.keyframes` とタイムラインキャンバスを更新
|
||
|
||
### Changed
|
||
- **Row A / Row B レイアウト変更** — 再生・停止ボタンを Row A から Row B へ移動し、操作グループを整理:
|
||
- Row A から `sep2`・`▶(再生)`・`■(停止)` を削除
|
||
- Row A 最終形: `[+KF] [🗑KF] | [+CK] [-CK] | [↔] | [0] [◀] [f] / [t] [▶(次フレーム)]`
|
||
- `[🎬 WebM]` を Row B の `[💾 Proj]` の直右隣に移動(`margin-left:auto` を除去)
|
||
- Row B に `flex:1` スペーサー (`rowBSpacer`) を追加し、右端に `[▶(再生)] [■(停止)]` を配置
|
||
- 再生ボタンに `min-width:54px`(通常の約 2 倍幅)を追加
|
||
- Row B 最終形: `[New] FPS [24] [💾 Proj] [🎬 WebM] ←flex:1→ [進捗] [▶▶▶] [■]`
|
||
|
||
- **i18n** — `kfMoveKeyBtn` / `kfMoveKeyTooltip` を ja / en / zh に追加
|
||
|
||
---
|
||
|
||
## v0.5.0 — 2026-06-18
|
||
|
||
### Overview
|
||
キーフレームアニメーションシステムと WebM 動画エクスポートを追加。⏱ ボタンでキーフレームパネルを展開し、ポーズ / カメラを独立してフレームに記録、タイムラインをクリック / ドラッグでスクラブ、補間再生プレビュー、WebM 形式での動画書き出しができる。カメラキーフレームはポーズとは独立した補間ロジックで処理される。キーフレームプロジェクトはライブラリのポーズ欄に保存・再読み込みでき、`layer_config.keyframes` に永続化されるため ComfyUI のワークフロー保存にも対応。
|
||
|
||
### Added
|
||
- **ポーズキーフレーム記録 (`+KF` / `🗑KF`)** — 現在フレームにポーズを記録・削除。位置 (tx/ty)・角度・可視性・SW 角度を保存。カメラは含まない(後述の `+CK` で個別に記録)
|
||
|
||
- **カメラキーフレーム記録 (`+CK` / `-CK`)** — 現在フレームにカメラ状態(zoom / x / y / roll)を個別に記録・削除。同フレームにポーズ KF があれば `camera` フィールドを追加 / 更新し、なければカメラのみのエントリを新規作成。`-CK` はカメラフィールドだけを削除しポーズデータは保持する
|
||
|
||
- **タイムライン UI (`⏱ トグル`)** — ノードの row3 に ⏱ ボタンを追加。展開時にキーフレームパネルを表示:
|
||
- **Row A**: `[+KF] [🗑KF] | [+CK] [-CK] | [0] [◀] [フレーム番号] / [総フレーム数] [▶] | [▶ 再生] [■ 停止]`
|
||
- **タイムラインキャンバス** (34 px) — フレームトラック + ◆ マーカー + 再生ヘッドを描画。クリック / ドラッグでスクラブ
|
||
- **Row B**: `[New] FPS [入力] | [💾 Proj] | [進捗表示] | [🎬 WebM]`
|
||
|
||
- **タイムラインマーカーの色分け** — キーフレームの種別を ◆ の色で識別:
|
||
- 黄(`#ffdd44`)— ポーズのみ
|
||
- 紫(`#cc66ff`)— カメラのみ
|
||
- 緑(`#44ee88`)— ポーズ+カメラ両方
|
||
- 現在フレームのマーカーは 1px 大きく同色グローを表示
|
||
|
||
- **`0` ボタン(フレーム 0 ジャンプ)** — Row A のナビゲーション左端に配置。`seekToFrame(node, 0)` を呼ぶ
|
||
|
||
- **`New` ボタン(全 KF クリア)** — Row B の FPS 左隣に配置。confirm ダイアログ後に `node._keyframes = []` / `layer_config.keyframes` 削除 / フレーム 0 にリセット。誤操作防止のため Row B(操作頻度の低い行)に配置
|
||
|
||
- **フレーム補間** — キーフレーム間を自動補間:
|
||
- 位置 (tx/ty): 線形補間 (lerp)
|
||
- 角度 / SW 角度 / カメラ roll: 最短回転パスで補間 (shortest-path angle lerp)
|
||
- カメラ zoom / x / y: 線形補間
|
||
- カメラ補間はポーズ KF の位置とは独立し、`camera` フィールドを持つ KF のみを対象に前後を探して補間する
|
||
- 可視性: 直前のキーフレーム値を維持(ステップ)
|
||
|
||
- **再生プレビュー** — `▶` ボタンで RAF ベースの再生ループ。FPS フィールドで速度を調整(デフォルト 24 fps)。再生中は `■` で停止
|
||
|
||
- **WebM 動画エクスポート (`🎬 WebM`)** — `canvas.captureStream(0)` + `track.requestFrame()` + `MediaRecorder` でフレームを逐次レンダリングしてエンコード。Chrome / Edge 推奨(VP8 コーデック)。カメラキーフレームは `renderToOutputCanvas` が `node._camera` を参照するため自動的に WebM に反映される。進捗をテキストで表示
|
||
|
||
- **プロジェクト保存 / 読み込み (`💾 Proj`)** — キーフレームデータ(フレームリスト・総フレーム数・FPS)をライブラリのポーズ欄に保存。ファイル名デフォルトは `project-YYYYMMDDHHMMSS`(プロンプトで変更可)。ライブラリから読み込み時に `_type: "kf_project"` フィールドで自動判別し、フレーム 0 のポーズをキャンバスに適用してタイムラインを復元
|
||
|
||
- **キーフレームのワークフロー永続化** — キーフレームは `layer_config.keyframes` に保存。ComfyUI のワークフロー保存 / 読み込みで自動復元。`PSDModal._apply()` による設定上書き時もキーフレームが保持される (`onConfigure` フックで `_keyframes` を再展開)
|
||
|
||
- **新定数 `KF_PANEL_H = 98`** — キーフレームパネルの高さ定数を `computeSize` と `buildKeyframePanel` で共用
|
||
|
||
- **i18n キー追加** (`i18n.js`) — `kfPanelBtn`, `kfPanelTooltip`, `kfAddBtn`, `kfAddTooltip`, `kfDelBtn`, `kfDelTooltip`, `kfPlayBtn`, `kfStopBtn`, `kfExportBtn`, `kfExportTooltip`, `kfExporting`, `kfFpsLabel`, `kfNoKeyframes`, `kfSaveProjBtn`, `kfSaveProjTooltip`, `kfProjNamePrompt`, `kfGoToZeroBtn`, `kfGoToZeroTooltip`, `kfClearBtn`, `kfClearTooltip`, `kfAddCamBtn`, `kfAddCamTooltip`, `kfDelCamBtn`, `kfDelCamTooltip` を ja / en / zh に追加
|
||
|
||
### Fixed
|
||
- **KF パネル展開時のノード高さ崩れ** (`psd_loader.js`) — `nodeType.prototype.computeSize` がハードコードの `UI_WIDGET_H + 80` を返していたため、ComfyUI がレンダリングサイクルごとにノード高さを旧値に戻し、パネルが見えなくなっていた。`_kfPanelVisible` フラグを参照して `const kfExtra = (this._kfPanelVisible ? KF_PANEL_H : 0)` を加算するよう修正
|
||
|
||
- **カメラのみ KF でシーク / 再生がクラッシュしてループが止まる** (`seekToFrame`) — `_kfGetInterpolatedState` がカメラのみ KF をそのまま返すと `state.pose` / `state.sw_angles` / `state.visibility` が `undefined` になる。`JSON.parse(JSON.stringify(undefined))` が SyntaxError をスローし RAF ループが停止していた。各フィールドに `undefined` ガードを追加(`state.pose !== undefined` のときのみ適用、`state.sw_angles` / `state.visibility` は `&&` でガード)
|
||
|
||
- **`+KF` でポーズを上書きするとカメラ KF が消える** (`addKeyframeAtCurrentFrame`) — 同フレームの既存 KF を `.filter` で丸ごと削除してから新 KF を追加していたため、`camera` フィールドが失われていた。上書き前に `existingKf = node._keyframes.find(k => k.frame === frame)` を取得し、`existingKf?.camera` があれば新 KF に引き継ぐよう修正
|
||
|
||
- **`🗑KF` がカメラ KF も削除する** (`deleteKeyframeAtCurrentFrame`) — 丸ごと削除していたため同フレームのカメラデータも失われていた。ポーズ系フィールド(`pose` / `sw_angles` / `visibility`)のみ除去し `camera` は保持するよう修正。`camera` もなくなった場合のみエントリを削除する
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
// layer_config に追加されるキーフレームフィールド
|
||
{
|
||
"keyframes": [
|
||
{
|
||
"frame": 0,
|
||
"visibility": { "<layerId>": true },
|
||
"pose": { "<layerId>": { "angle": 0, "tx": 0, "ty": 0 } },
|
||
"sw_angles": { "<pointId>": 0 },
|
||
"camera": { "zoom": 1.0, "x": 0, "y": 0, "roll": 0 } // +CK で記録、省略可
|
||
}
|
||
],
|
||
"kf_total_frames": 60,
|
||
"kf_fps": 24
|
||
}
|
||
```
|
||
`keyframes` がない既存の設定は従来通り動作(後方互換)。`camera` フィールドがない KF ではカメラは変化しない。
|
||
|
||
---
|
||
|
||
## v2.24.0 — 2026-06-10
|
||
|
||
### Overview
|
||
Post-release code review of the v2.21–v2.23 work (clipping-mask support, +L/+P/+C split)
|
||
surfaced nine findings; eight are fixed here. The ninth — in-place entry-type conversion
|
||
between layer and group entries, removed by the +L/+P/+C dropdown split — is accepted as a
|
||
design tradeoff. Highlights: clipping stacks now work at the document root and inside
|
||
custom groups (previously only inside PSD folders), layer masks are applied again in
|
||
canvas layer images, hidden clipping layers are respected by server-side compositing, and
|
||
the clipping-stack offscreen canvas is cached instead of re-allocated per frame.
|
||
Additionally, the output-size widgets now step in 1 px increments.
|
||
|
||
### Fixed
|
||
- **Root-level / custom-group clipping stacks** (`psd_loader.js`) — clipping stacks were
|
||
only assembled by `renderChildren`, which is reached exclusively for PSD folder children.
|
||
Root-level layers (both the `cg_order` path and the PSD-order fallback) and custom-group
|
||
members were drawn one-by-one via `renderOneNode` / `renderCg`, so clipping layers
|
||
outside folders rendered unclipped (fully visible). New helper
|
||
`renderIdList(ids, skipCgMembers, skipCgMemberEntries)` groups consecutive plain nodes
|
||
into runs and feeds them through `renderChildren`, so stacks form on every path:
|
||
- `cg_order` path → `renderIdList([...cgOrder].reverse(), true, true)`
|
||
- `renderCg` (custom-group members) → `renderIdList([...cg.layer_ids].reverse(), false, false)`
|
||
- fallback path → `renderChildren(layers, false)` directly
|
||
- `renderClippingStack` also gained the previously missing
|
||
`skipCgMembers && cgMemberIds.has(base.id)` guard on the base layer, matching
|
||
`renderOneNode` behavior.
|
||
|
||
- **Hidden clipping layers baked into server-side composite** (`psd_utils.py`) —
|
||
`_manual_composite` / `_manual_composite_ordered` skipped clipping layers *before*
|
||
consulting `effective_vis`, and the base layer's `composite()` bakes clip layers in
|
||
regardless of config — so a clipping layer hidden in the UI still appeared in
|
||
`/psd_loader/preview` output (JS preview hid it correctly). Base composition now passes
|
||
a `layer_filter` built from `effective_vis` (`_composite_single(layer, vis_fn)`), so
|
||
hidden clipping layers are excluded. psd-tools without `layer_filter` support falls back
|
||
to the previous behavior via `TypeError` catch.
|
||
|
||
- **Layer masks lost in canvas layer images** (`psd_utils.py::get_layer_image_by_id`) —
|
||
v2.21 switched to `topil()` to avoid clip-layer double-draw, but `topil()` returns raw
|
||
pixels without layer masks, effects, or opacity. Now uses
|
||
`composite(layer_filter=lambda l: l is current)`, which applies masks while still
|
||
excluding clipping layers. For clipping layers themselves, a solo composite produces a
|
||
fully transparent image (nothing to clip to) — fixed by temporarily clearing the
|
||
`clipping` flag during composite (restored in `finally`; verified alpha-identical to
|
||
`topil()` on the sample PSD).
|
||
|
||
### Performance
|
||
- **Offscreen canvas cached** (`psd_loader.js`) — `renderClippingStack` allocated a
|
||
full-size `document.createElement('canvas')` per clipping stack per render pass;
|
||
`_drawPreview` runs on every mousemove during pose dragging, so this caused GC churn and
|
||
frame drops. The canvas is now cached in module-scope `_clipTmpCanvas`, resized only when
|
||
dimensions change, and cleared with `clearRect` per use.
|
||
|
||
### Changed
|
||
- **`output_width` / `output_height` step 64 → 1** (`psd_loader_node.py`) — output size is
|
||
now adjustable in 1 px increments (drag/arrow steps; was jumping by 64).
|
||
- **`countSwSlots` = `expandSwGroupEntries().length`** (`psd_loader.js`) — removed the
|
||
duplicated composite/piece counting logic; slot expansion now has a single source of
|
||
truth (verified equivalent across 7 entry-type cases).
|
||
- **`isOrphaned` simplified** (`_renderSwitchTab`) — the composite/piece ternary collapsed
|
||
to `(isPsdGroup || isCustomGroup) && (leaves?.length ?? 0) === 0`, which is equivalent
|
||
for both modes.
|
||
- **`server.py::_build_layer_tree` delegates to `psd_utils.get_layer_tree`** — removed the
|
||
duplicated layer-dict construction (the two copies had to be edited in lockstep, e.g.
|
||
the v2.21 `clipping` field). Output verified identical.
|
||
- **`_paste_layer` helper extracted** (`psd_utils.py`) — the identical composite→RGBA→
|
||
bounds-check→paste block in `_manual_composite` and `_manual_composite_ordered` is now
|
||
shared.
|
||
- **Error logging** — the bare `except Exception: pass` around the layer-image composite
|
||
now logs `[psd_figure_creator] layer composite error (<name>): <e>` instead of silently
|
||
swallowing corrupt-layer failures.
|
||
- **Dead i18n key removed** — `addGroupTooltip` (ja/en/zh) had no remaining references
|
||
after the v2.20 +L/+P/+C button split.
|
||
|
||
### Verification
|
||
- Python (sample_1.psd): hiding clipping layer `6.1` (irides-r) now changes both manual
|
||
composite paths (3,360 px diff, matching the layer bbox); all-visible output is
|
||
byte-identical before/after; `get_layer_image_by_id` returns mask-applied, clip-free
|
||
base images and non-empty clip-layer images.
|
||
- JS (stub-DOM harness, 9 tests): clipping stacks form on root fallback / `cg_order` /
|
||
custom-group paths; `source-atop` draw order preserved; visibility overrides respected;
|
||
canvas cache allocates once; `countSwSlots` equivalence; non-clipping layers still draw
|
||
directly to the main canvas.
|
||
|
||
---
|
||
|
||
## v2.23.0 — 2026-06-07
|
||
|
||
### Overview
|
||
Bundled a sample character PSD with a pre-configured model and two pose files so users can
|
||
try the rigging system immediately after installation.
|
||
|
||
### Added
|
||
- **`user_data/sample_1.psd`** — sample character PSD (~2.3 MB).
|
||
- **`user_data/models/sample.psd-model.json`** — fully configured model for `sample_1.psd`:
|
||
- R pivots on layers 2, 3, 4, 11
|
||
- MR points on layers 0, 1, 5, 9, 10, and sub-layers 6.1 / 7.1
|
||
- Parent hierarchy: layer 2 as root → 3 → 4 → children (eyes, mouth, accessories, etc.)
|
||
- SW layer `SW1` with a composite-mode slot referencing group 7 and leaf layer 8
|
||
- **`user_data/poses/pose1.pose.json`** — sample pose 1 (head tilt + eye/mouth movement).
|
||
- **`user_data/poses/pose2.pose.json`** — sample pose 2 (alternate expression).
|
||
|
||
### Notes
|
||
- `user_data/models/` and `user_data/poses/` are loaded automatically by the library panel.
|
||
- `user_data/sample_1.psd` must be copied to `ComfyUI/input/psd/` manually before the model
|
||
can be loaded (the model JSON references `psd_filename: "sample_1.psd"`).
|
||
|
||
---
|
||
|
||
## v2.22.0 — 2026-06-07
|
||
|
||
### Overview
|
||
Two maintenance fixes: a DOM crash in inline-rename handlers, and a help-dialog gap where
|
||
the Chinese Parent Tab section was missing entirely and all three languages lacked the
|
||
clipping-layer caveat for parent setup.
|
||
|
||
### Fixed
|
||
- **Inline rename `replaceWith` crash** (`psd_loader.js`) — Four inline-rename handlers
|
||
(SW point name, custom-group name, SW layer name, `_inlineRename`) all shared the same
|
||
pattern: `nameEl.replaceWith(input)` on double-click, then `input.replaceWith(nameEl)` in
|
||
`commit()`. Pressing Enter called `commit()` which removed `input` from the DOM, then the
|
||
resulting `blur` event fired `commit()` a second time, causing
|
||
`NotFoundError: Failed to execute 'replaceWith' on 'Element'`. Fixed by adding
|
||
`if (!input.isConnected) return;` at the top of every `commit` closure — idempotent once
|
||
the input is detached.
|
||
|
||
### Changed
|
||
- **Help dialog — Parent Tab** (`_showHelp` in `psd_loader.js`):
|
||
- Added `⚠ クリッピングレイヤー` row to the Japanese **ペアレントタブ** section.
|
||
- Added `⚠ Clipping Layers` row to the English **Parent Tab** section.
|
||
- Added the entire **父级选项卡** section to the Chinese help — it was previously absent.
|
||
The new section includes setup instructions and the same clipping-layer caveat.
|
||
- Caveat text (all languages): clipping layers (✂) only follow the base layer's transform
|
||
if both layers share the same parent in the Parent tab; if only the base has a parent,
|
||
the clipping layer stays at its original canvas position and the mask alignment breaks.
|
||
|
||
---
|
||
|
||
## v2.21.0 — 2026-06-07
|
||
|
||
### Overview
|
||
Full clipping-mask support across the canvas preview and Capture output. Clipping layers
|
||
(layers with the Photoshop "clip to layer below" flag) are now detected, propagated to the
|
||
frontend, and rendered using an offscreen-canvas `source-atop` compositing technique.
|
||
R/MR rigs placed on a clipping layer work correctly while the clipping shape is enforced by
|
||
the base layer's alpha. Additionally, double-rendering of clipping layers in the server-side
|
||
manual compositor is fixed.
|
||
|
||
### Added
|
||
- **`clipping` field on layer-tree nodes** — `server.py::_build_layer_tree` and
|
||
`psd_utils.py::_build_layer_node` now emit `"clipping": true|false` for every layer node.
|
||
This was the root cause of clipping not working: the frontend received nodes without the
|
||
flag, so all layers appeared as non-clipping.
|
||
|
||
- **`renderChildren(children, skipCgMembers)`** — new function inside `renderLayersToCtx`
|
||
that replaces direct `for...of n.children` iteration. It scans the child array and groups
|
||
consecutive clipping layers with their base layer into a "clipping stack", delegating to
|
||
`renderClippingStack` when clips are found.
|
||
|
||
- **`renderClippingStack(base, clipLayers, skipCgMembers)`** — offscreen-canvas compositor:
|
||
1. Creates a temporary canvas the same size as the main canvas.
|
||
2. Copies the current camera transform to the offscreen context via `ctx.getTransform()` /
|
||
`tmpCtx.setTransform()`.
|
||
3. Temporarily swaps the closed-over `ctx` variable to `tmpCtx`; all drawing functions
|
||
(`drawLeaf`, `applyRigTransform`) automatically target the offscreen canvas.
|
||
4. Draws the base layer, then draws each clipping layer with
|
||
`globalCompositeOperation = 'source-atop'` (clips to base's opaque pixels).
|
||
5. Resets the transform and composites the offscreen canvas onto the main canvas 1:1.
|
||
- R/MR rigs on clipping layers are fully applied inside `drawLeaf` before the
|
||
`source-atop` clip is enforced, so both transform and clipping work simultaneously.
|
||
|
||
- **`✂` badge in layer panel** — `_mkLayerEl` appends a small `✂` span when `node.clipping`
|
||
is true, giving visual feedback in the Layers tab and the SW `+L` dropdown.
|
||
|
||
- **i18n key `clippingLayerBadge`** added to ja / en / zh for the badge tooltip.
|
||
|
||
### Fixed
|
||
- **`get_layer_image_by_id` (canvas layer fetch)** — switched from `layer.composite()` to
|
||
`layer.topil()` for non-group layers. `composite()` embeds associated clipping layers into
|
||
the base layer's image, causing the clipping layer to appear twice on the canvas and making
|
||
R/MR rigs on those layers invisible. `topil()` returns raw pixel data only; clipping is
|
||
handled entirely by `renderClippingStack` at draw time.
|
||
|
||
- **`_manual_composite` / `_manual_composite_ordered`** — clipping layers are now skipped
|
||
(`if getattr(layer, "clipping", False): return`). The base layer's `layer.composite()`
|
||
already includes its clipping layers, so iterating and drawing them separately caused
|
||
double rendering in the server-side compositor (both routes: `layer_order` and fallback).
|
||
|
||
### Changed
|
||
- **`renderLayersToCtx` signature** — parameter renamed from `ctx` to `ctxArg`; internal
|
||
`let ctx = ctxArg` allows the variable to be temporarily reassigned to the offscreen
|
||
context inside `renderClippingStack` without touching any call sites.
|
||
- **`renderOneNode` group path** — the two inline `for (const child of n.children)` loops
|
||
are replaced by `renderChildren(n.children, skipCgMembers)`.
|
||
|
||
---
|
||
|
||
## v2.20.0 — 2026-06-07
|
||
|
||
### Overview
|
||
The single `+` button in the Switch tab is replaced by three distinct add buttons —
|
||
`+L` (Layer), `+P` (Piece), `+C` (Composite) — each producing a different slot entry
|
||
type. A new `mode: 'composite'` field on group/folder entries makes the entire group count
|
||
as a single slot whose member layers are composited together, contrasting with
|
||
`mode: 'piece'` (the existing per-layer expansion behavior). Each entry row now shows a
|
||
color-coded type badge and the dropdown is filtered to show only relevant options for
|
||
that entry type.
|
||
|
||
### Added
|
||
- **`+L` / `+P` / `+C` buttons** — replace the single `+` button in the Switch tab bar:
|
||
- **`+L` (Layer)** — adds an individual PSD layer as a string entry (1 slot); dropdown
|
||
shows leaf layers only
|
||
- **`+P` (Piece)** — adds a custom group or PSD folder group with `mode: 'piece'`
|
||
(default expansion behavior: 1 slot per member/leaf layer); dropdown shows groups and
|
||
folders only
|
||
- **`+C` (Composite)** — adds a custom group or PSD folder group with
|
||
`mode: 'composite'` (entire group = 1 slot; all member layers rendered together);
|
||
dropdown shows groups and folders only
|
||
- `makeAddSwBtn(text, tipKey, mode)` helper in `_buildDOM` creates all three buttons
|
||
- i18n keys added: `addLayerEntryTooltip`, `addPieceEntryTooltip`,
|
||
`addCompositeEntryTooltip`, `noAssignableGroup` (ja/en/zh)
|
||
|
||
- **`mode: 'composite'` entry field** — new optional field on `psd_group` / `custom_group`
|
||
entries in `pt.groups`:
|
||
- Backward-compatible: entries without `mode` (all v2.19 data) continue to behave as
|
||
`'piece'`
|
||
- `expandSwGroupEntries` — composite branch returns a single slot
|
||
`{ id, entryIdx, mode:'composite', memberIds:[...] }` instead of per-leaf entries
|
||
- `countSwSlots` — composite entries always contribute 1, regardless of member count
|
||
- `renderLayersToCtx` SW loop — composite slot activates/deactivates all `memberIds`
|
||
together via a loop over `slot.memberIds`; piece slot still controls a single `id`
|
||
|
||
- **Type badges in Switch tab entry rows** (`[L]` / `[P]` / `[C]`):
|
||
- Rendered as a small colored `<span>` to the left of the step-angle label
|
||
- L → blue (`#89b4fa` on `#2a2a4a`), P → cyan (`#89dceb` on `#152535`),
|
||
C → orange (`#fab387` on `#3a2515`)
|
||
|
||
### Changed
|
||
- **`_addSwGroup(mode)`** — gained `mode` parameter (`'layer'` | `'piece'` | `'composite'`):
|
||
- `'layer'`: pushes the first leaf layer ID (string) found in `layerTree`
|
||
- `'piece'` / `'composite'`: pushes `{ type:'custom_group'|'psd_group', id, mode }`
|
||
preferring the first custom group, falling back to the first PSD folder group
|
||
- **`_renderSwitchTab` entry loop**:
|
||
- `slotCount` set to `1` for composite entries (was `leaves.length`)
|
||
- Orphan detection for composite: checks `leaves.length === 0` (group/folder empty
|
||
or deleted) rather than `slotCount === 0`
|
||
- Angle range label: range notation (`0°–60°`) suppressed for composite (always
|
||
single angle)
|
||
- Dropdown options filtered by entry type: string entries show layers only; object
|
||
entries show custom groups + PSD folder groups only
|
||
- Change handler preserves `entry.mode` when updating the selected group/folder:
|
||
`pt.groups[i] = { type, id, mode: entry?.mode ?? 'piece' }`
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
// pt.groups entry (new mode field)
|
||
{ "type": "custom_group", "id": "...", "mode": "piece" } // +P: per-layer slots
|
||
{ "type": "psd_group", "id": "...", "mode": "composite" } // +C: 1 composite slot
|
||
```
|
||
Entries without `mode` default to `'piece'` (backward compatible with v2.19 data).
|
||
|
||
---
|
||
|
||
## v2.19.0 — 2026-06-06
|
||
|
||
### Overview
|
||
Four improvements: (1) thumbnails for model/pose/pose+SW saves are now always captured with
|
||
rig labels and the output-size label hidden, then the UI state is restored; (2) the Switch tab
|
||
now accepts PSD folder groups as expandable per-layer slot entries; (3) custom groups in the
|
||
Switch tab also expand per-layer (matching PSD folder behavior); (4) modal pose-save thumbnails
|
||
no longer show a dark outer frame caused by camera zoom state.
|
||
|
||
### Fixed
|
||
- **Dark outer frame in modal pose-save thumbnails** — `_savePoseFromModal()` /
|
||
`_savePoseWithSwFromModal()` computed `_computeModalOutputFrame()` under the current camera
|
||
state; when the user had zoomed in, the output frame extended beyond the preview canvas
|
||
bounds and `drawImage` captured dark canvas background around the content:
|
||
- Before capture: `savedCam = { ...this._previewCam }`, then reset to
|
||
`{ ...this._defaultPreviewCam }` so the output frame is always within canvas bounds
|
||
- After capture: restore `this._previewCam = savedCam` and call `_drawPreview()` to return
|
||
to the user's camera state
|
||
- **Rig labels and output-size label visible in saved thumbnails** — model, pose, and
|
||
pose+SW saves captured thumbnails while `node._showRigLabels` and the `512 × 512` frame
|
||
label were still active; replaced `createNodeCanvasNoLabel` (offscreen canvas — incorrectly
|
||
produced small/dark results) with `captureThumbFromNode`:
|
||
- Save `node._showRigLabels`; set to `false`
|
||
- Call `drawNodeCanvas(node, { skipFrameLabel: true })` — suppresses both rig labels and
|
||
the output-size text
|
||
- Crop and export; restore `node._showRigLabels`; call `drawNodeCanvas(node)` to redraw
|
||
- Modal saves (`_savePoseFromModal`, `_savePoseWithSwFromModal`) apply the same pattern
|
||
using `this._showRigLabels` + `this._drawPreview()`
|
||
|
||
### Added
|
||
- **PSD folder groups as Switch entries** (`[フォルダ]` / `[Folder]` / `[文件夹]`):
|
||
- `getPsdGroupLeaves(groupId, layerTree)` — recursively collects all non-group descendant
|
||
IDs from a PSD folder node
|
||
- `expandSwGroupEntries(groups, layerTree, customGroups)` — flattens `pt.groups` entries
|
||
into a per-slot list; `string` → 1 slot, `{type:'psd_group'}` → N leaf slots,
|
||
`{type:'custom_group'}` → N `layer_ids` slots
|
||
- `countSwSlots(groups, layerTree, customGroups)` — sums total slot count across all
|
||
entries (dissolved entries contribute 0)
|
||
- Switch tab dropdown includes `[フォルダ] <name>` options for every PSD group node in the
|
||
layer tree (recursive); stored as `{ type: 'psd_group', id }` in `pt.groups`
|
||
- Angle display: 1 slot → `"30°"`, multiple slots → `"0°–60°"` range notation
|
||
- Orphaned entry (PSD folder dissolved): red row background, ⚠ tooltip icon, angle shown
|
||
as `"-"`; entry must be deleted manually
|
||
- Max-slot guard (`> 12`) applies across all entry types combined
|
||
- i18n keys added: `psdGroupPrefix` (ja/en/zh), `swGroupOrphaned` (ja/en/zh)
|
||
|
||
- **Custom groups as Switch entries** (`[グループ]` / `[Group]` / `[组]`) — expand per-layer:
|
||
- Previously custom groups were stored as plain string IDs in `pt.groups` and treated as
|
||
single slots with no actual rendering effect
|
||
- Now stored as `{ type: 'custom_group', id }`; `expandSwGroupEntries` expands each
|
||
custom group into per-`layer_ids` slots, identical to PSD folder expansion
|
||
- Dropdown option values changed from `cg2.id` to `cg:<id>` to distinguish from layer IDs;
|
||
change handler recognizes `cg:` prefix and writes `{ type: 'custom_group', id }` to
|
||
`pt.groups[i]`
|
||
- `_addSwGroup()` default new entry for custom groups is now `{ type:'custom_group', id }`
|
||
instead of a bare string
|
||
- Orphaned handling: if the custom group no longer exists in `this.state.customGroups`,
|
||
`slotCount = 0` → shown as orphaned with ⚠ (same as PSD folder)
|
||
- `renderLayersToCtx` and `drawRigOverlay` both pass `cgList` / `customGroups` to
|
||
`expandSwGroupEntries` so SW visibility switching works for all entry types
|
||
|
||
### Changed
|
||
- **`drawNodeCanvas` signature** — added `{ skipFrameLabel = false, targetCanvas = null }`
|
||
options object; output-size label block is wrapped in `if (!skipFrameLabel)` guard
|
||
- **`captureThumbFromNode(node, thumbW, thumbH)`** — replaces the removed
|
||
`createNodeCanvasNoLabel`; uses the "temporarily disable → draw → capture → restore"
|
||
pattern on `node._nodeCanvas`; used by all 5 thumbnail capture paths:
|
||
model save (140×140), modal pose save fallback (140×auto), modal pose+SW save fallback,
|
||
quick pose save (node contextmenu), quick pose+SW save (node contextmenu)
|
||
|
||
### Updated
|
||
- **Help dialog** (`_showHelp`) — all three languages (ja/en/zh) updated:
|
||
- Switch tab section fully rewritten: entry types ([Group]/[Folder]/[Layer]), slot
|
||
expansion behavior, angle range notation, 12-slot max, orphaned ⚠ warning
|
||
- Pose mode: note added that thumbnails are captured with labels hidden automatically
|
||
- Layer tab: custom group creation description mentions SW tab per-layer expansion
|
||
- Camera (modal preview): RC reset button row added
|
||
- Chinese (zh): Switch tab and Pose mode sections added (were previously absent)
|
||
|
||
---
|
||
|
||
## v2.18.0 — 2026-06-05
|
||
|
||
### Overview
|
||
Two improvements: (1) fixed a broken JS module import that prevented `psd_pose_editor.js` from
|
||
loading; (2) added a "?" help button to the PSD Model Editor footer that opens a scrollable
|
||
in-app help dialog in the user's language (ja/en/zh). Also added screenshots to all three
|
||
README files.
|
||
|
||
### Fixed
|
||
- **`psd_pose_editor.js` fails to load on ComfyUI startup** (`vite:preloadError`):
|
||
- Line 1 used a relative import `../../scripts/app.js` which resolves to
|
||
`/extensions/scripts/app.js` (non-existent) instead of `/scripts/app.js`
|
||
- Changed to absolute path `/scripts/app.js`, matching `psd_loader.js` convention
|
||
- The error appeared twice in the console because ComfyUI's Vite layer reported the
|
||
same failed fetch from two code paths (`console-listener.js` direct + `App.vue:73`)
|
||
|
||
### Added
|
||
- **In-app help dialog** — "?" button added to the bottom-left of the PSD Model Editor footer:
|
||
- Uses `margin-right: auto` on the button to left-align it while keeping all other footer
|
||
buttons (`Cancel` / `Save` / `Pose` / `Apply`) right-aligned
|
||
- `_showHelp()` method on `PSDModal`: builds a scrollable overlay dialog with sections
|
||
covering Basic Operations, Layer Tab, Parent Tab, Switch Tab, Setup Mode, Pose Mode,
|
||
and Camera controls (modal vs. node preview distinguished separately)
|
||
- Content fully translated: Japanese / English / Simplified Chinese, selected via `getLang()`
|
||
- Dialog closes on outside click or the "Close" button; rendered at `z-index: 10001`
|
||
(above the main modal at 10000)
|
||
- `getLang` added to the `i18n.js` import in `psd_loader.js`
|
||
- **Help dialog CSS** (`.psd-help-dialog`, `.psd-help-header`, `.psd-help-body`,
|
||
`.psd-help-section`, `.psd-help-section-title`, `.psd-help-table`, `.psd-help-term`,
|
||
`.psd-help-desc`) — dark-themed: blue section titles (`#89b4fa`), purple term
|
||
cells (`#cba6f7`), muted description text (`#bac2de`)
|
||
- **i18n keys** `helpBtn`, `helpTitle`, `helpClose` added to ja / en / zh locales
|
||
- **README screenshots** — `## Screenshots` section inserted between Features and Installation
|
||
in `README.md`, `README.ja.md`, `README.zh.md`; includes all 7 images from `docs/`
|
||
(`1_node.png` – `7_capture.png`, excluding `thumb.png`) with language-appropriate captions
|
||
|
||
### Fixed (help content accuracy)
|
||
- Camera operation descriptions corrected after code review:
|
||
- Roll is **not** available in the modal preview (`_previewCam` has no `roll` property;
|
||
right-drag in the modal pans, not rolls)
|
||
- Roll is **only** available on the node preview canvas: `Alt + right-drag`
|
||
(`setupPreviewInteraction` → `e.altKey && e.button === 2`)
|
||
- Camera reset button on the node is **RC** (not ↺ as initially written)
|
||
- Help now shows two separate camera sections: "Modal Preview" (zoom + pan only) and
|
||
"Node Preview" (zoom + pan + roll + RC reset)
|
||
|
||
---
|
||
|
||
## v2.17.0 — 2026-06-05
|
||
|
||
### Overview
|
||
Two maintenance updates: (1) plugin renamed to "PSD Figure Creator"; (2) i18n support added for
|
||
Japanese, English, and Simplified Chinese.
|
||
|
||
### Changed
|
||
- **プラグイン名変更** — "PSD Loader" → "PSD Figure Creator":
|
||
- `psd_loader_node.py`: クラス名 `PSDLoaderNode` → `PSDFigureCreatorNode`、
|
||
`NODE_CLASS_MAPPINGS` / `NODE_DISPLAY_NAME_MAPPINGS` のキー・値を更新
|
||
- `web/js/psd_loader.js`: `NODE_TYPE = "PSDFigureCreator"`、
|
||
extension 登録名 `"psd_loader.PSDFigureCreator"` に更新
|
||
- `psd_utils.py`: ログラベル `[psd_loader]` → `[psd_figure_creator]`
|
||
- **注意**: 既存ワークフローの JSON に `"PSDLoader"` が保存されている場合は
|
||
`"PSDFigureCreator"` に書き換えが必要
|
||
|
||
---
|
||
|
||
## v2.16.0 — 2026-06-05
|
||
|
||
### Overview
|
||
Security hardening: path traversal prevention across all file-handling endpoints, and
|
||
i18n (internationalization) support for Japanese / English / Simplified Chinese.
|
||
|
||
### Security
|
||
- **パストラバーサル修正** (`server.py`):
|
||
- `/psd_loader/upload`: `field.filename` を `Path(...).name` でサニタイズ後、
|
||
`save_path.resolve().is_relative_to(psd_dir.resolve())` で境界チェックを追加
|
||
- `/psd_loader/layers`, `/psd_loader/layer_image`, `/psd_loader/preview`:
|
||
クエリパラメータ `filename` を `Path(...).name` でディレクトリ成分除去
|
||
- `/psd_loader/preview`: `width` パラメータを `min(..., 4096)` で上限制限
|
||
- エラーメッセージからファイルパスを除去(内部情報漏洩対策)
|
||
- **パストラバーサル修正** (`psd_loader_node.py`):
|
||
- `psd_dir / psd_filename` → `psd_dir / Path(psd_filename).name`
|
||
- library 系エンドポイント(`/psd_loader/library/...`)は既存の `".."` チェックで対応済み
|
||
|
||
### Added
|
||
- **i18n 対応** — `web/js/i18n.js` を新規作成:
|
||
- 対応言語: 日本語 (`ja`)・英語 (`en`)・簡体字中国語 (`zh`)
|
||
- `navigator.language` で起動時に自動検出(`zh*` → zh、`ja` → ja、それ以外 → en)
|
||
- `t(key, ...args)` 関数: `{0}` プレースホルダーで変数埋め込みに対応
|
||
- `setLang(lang)` / `getLang()` でプログラムからの言語切り替えが可能
|
||
- 翻訳キー数: 約 65 キー(ボタン・ツールチップ・アラート・confirm・prompt・キャンバステキスト)
|
||
- **`psd_loader.js` の全日本語文字列を `t()` に置換**:
|
||
- `alert()` / `confirm()` / `prompt()` のメッセージ
|
||
- `.textContent` / `.title` / `.placeholder` の UI テキスト
|
||
- キャンバスオーバーレイテキスト(外部背景接続中メッセージ)
|
||
- プレフィックス文字列(`[カスタム]` / `[SW]` / `[グループ]` / `[レイヤー]`)
|
||
|
||
---
|
||
|
||
## v2.15.0 — 2026-06-04
|
||
|
||
### Overview
|
||
Three UI improvements: (1) a New button in the setup modal file bar to start fresh without any
|
||
rigging; (2) tab hover/active style unified across all three tabs; (3) pose save button right-click
|
||
now saves switch state (SW angles) together with the pose instead of applying the last saved pose.
|
||
|
||
### Added
|
||
- **「✨ 新規」ボタン** — セットアップモーダルのファイルバーに追加(PSDボタン左横):
|
||
- クリックで確認ダイアログ → `layer_config` ウィジェットを `"{}"` にリセット
|
||
- リギング・ポーズ・SWレイヤー・SWポイントをすべてクリア
|
||
- `_rigMode` を `'normal'` に戻し、`_reloadFromNode()` でUI再描画
|
||
- PSDボタンと同じ `flex:1` を付与し両ボタンが均等幅になることで PSD ボタン幅を実質半減
|
||
- **`_savePoseWithSwFromModal()`** — モーダルの「📷 ポーズ」ボタン右クリック用の新メソッド:
|
||
- `sw_angles: {pointId: angle}` をポーズファイルに追加して保存
|
||
- SWポイントの角度のみ記録(x/y/radius などセットアップ座標は含まない)
|
||
|
||
### Fixed
|
||
- **ペアレントタブの白線** — `mkTabBtn` のインライン CSS を `border-left/right/top:none` から
|
||
`border:none` に変更。ホバー時に `.psd-btn:hover` の `border-color` が `#6c7086` に変化し
|
||
bottom border が白線として見えていた問題を解消
|
||
- **タブホバー挙動の不統一** — 上記修正により 3 タブすべてで `background` 変化のみが起きるよう統一
|
||
|
||
### Changed
|
||
- **タブ選択の赤枠表示** — `updateTabStyle()` を更新し、アクティブタブに
|
||
`outline: 2px solid #f38ba8` を追加(`outline-offset:-2px` でボタン内側に収める)。
|
||
非アクティブタブは `outline: none`
|
||
- **ポーズ保存ボタン右クリックの動作変更** (ノード・モーダル共通):
|
||
- 変更前: 直前に保存した `node._poseSnapshots[0]` を `layer_config` に適用
|
||
- 変更後: 現在のスイッチ状態(SW角度)込みでポーズを名前付き保存
|
||
- 保存成功時フラッシュ色: 左クリック = 緑 (`#44ff88`)、右クリック = オレンジ (`#ff8844`) で区別可能
|
||
- モーダルの「📷 ポーズ」ボタンに `contextmenu` ハンドラを追加して同様の右クリック保存に対応
|
||
- **`_loadPose()` でスイッチ状態を復元** — ポーズファイルに `sw_angles` が含まれる場合、
|
||
現在の `config.sw_layers` 内の各 SW ポイントの `angle` のみを上書き。
|
||
x/y/radius などのセットアップ情報は一切変更しない
|
||
- **`poseSnapBtn.title`** — `"ポーズを保存(右クリックで適用)"` →
|
||
`"ポーズを保存(右クリック: スイッチ状態込みで保存)"` に更新
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
// pose ファイル(右クリック保存時のみ追加)
|
||
{
|
||
"sw_angles": {
|
||
"pointId": number // SW ポイントの現在角度(radians)。pointId = swl.points[i].id
|
||
}
|
||
}
|
||
```
|
||
`sw_angles` がないポーズファイルは従来通り `visibility` / `pose` のみ適用(後方互換)。
|
||
|
||
---
|
||
|
||
## v2.14.0 — 2026-06-04
|
||
|
||
### Overview
|
||
SWレイヤーUIのバグ修正と、レイヤー並べ替えが描画に反映されない根本的な問題を修正。
|
||
|
||
### Fixed
|
||
- **SWレイヤー行の▽トグル削除** — SWレイヤーは展開不要なためツリートグルを除去。行がフラット表示に統一
|
||
- **SWレイヤー行のクリック選択改善** — 以前は🔀アイコン部分しか選択できなかった問題を修正。
|
||
行全体(名前テキスト含む)をクリックで選択可能に。名前はダブルクリックでリネーム(250ms タイマーで区別)
|
||
- **SW選択と通常レイヤー選択の排他制御** — SWレイヤーを選択した状態で通常レイヤーをクリックすると
|
||
両方が選択状態になっていた問題を修正。`_mkLayerEl` / `_mkCustomGroupEl` のクリック時に
|
||
`_selectedSwLayerId` をクリアするよう変更
|
||
- **SWグループにPSDレイヤーを直接割り当て可能** — スイッチパネルの+ボタンを押したとき
|
||
「カスタムグループがありません」エラーになっていた問題を修正。カスタムグループだけでなく
|
||
個別PSDレイヤーもSWグループに割り当て可能とし、セレクトに `[グループ] / [レイヤー]` で表示
|
||
- **選択済みSWポイントのドラッグ操作改善** — スイッチパネルでSWポイントを選択しても
|
||
キャンバス上での移動操作ができなかった問題を修正。`_selectedSwPointInfo` がある場合に
|
||
sw_origin の 28px/zoom 以内のクリックでドラッグ開始するよう拡張
|
||
- **レイヤー並べ替えが描画に反映されない問題を修正** — `renderLayersToCtx` が常に
|
||
PSD元順序(psd-tools 返却順)で描画していたため、UIでの並べ替えが表示に反映されなかった。
|
||
`config.cg_order` を使って描画順を制御するよう根本修正:
|
||
- `nodeMap` を構築し ID でノードを参照
|
||
- `cgMemberIds` でカスタムグループ所属レイヤーを追跡(二重描画防止)
|
||
- `cgOrder` がある場合は `[...cgOrder].reverse()` 順(下層→上層)で描画
|
||
- カスタムグループ内も `layer_ids` の逆順で描画
|
||
- `cgOrder` 未設定時は PSD 元順序にフォールバック
|
||
|
||
---
|
||
|
||
## v2.13.0 — 2026-06-04
|
||
|
||
### Overview
|
||
SWポイントの管理方式を全面刷新。従来は「カスタムグループごとにSWポイントを1つ配置する」仕組みだったが、**SWレイヤー**という専用コンテナを導入し、1つのSWレイヤーに複数のSWポイントを自由に配置できるようにした。SWポイントの座標もレイヤー相対からPSD絶対座標へ移行。
|
||
|
||
### Changed (Breaking)
|
||
- **データ構造変更** — `config.sw` (`{layerId: {...}}`) を廃止し `config.sw_layers`
|
||
(`[{id, name, points:[{id, name, x, y, radius, angle, groups:[cgId]}]}]`) に変更
|
||
- SWポイントはPSD絶対座標(旧: 配置レイヤーからの相対座標)
|
||
- `LayerState.sw` → `LayerState.swLayers`
|
||
- `toConfig()` が `sw_layers` を出力、`fromConfig()` が読み込む
|
||
- 旧形式 `config.sw` は `fromConfig` 時に自動変換(後方互換)
|
||
|
||
### Added
|
||
- **SWレイヤー作成・削除ボタン** — レイヤータブ下部のボタンバー(グループ解除ボタン右隣)に
|
||
「SW追加」「SW削除」ボタンを追加
|
||
- **SWレイヤーのレイヤーリスト表示** — レイヤーリスト先頭に紫ボーダー付きの
|
||
`[SW] <名前>` 行として表示。クリックで選択(ダブルクリックでリネーム)
|
||
- **複数SWポイントの配置** — SWレイヤーを選択 → SWボタン → キャンバスクリックでSWポイントを配置。
|
||
配置のたびにSWボタンが自動解除され、再度押すことで次のポイントを追加できる
|
||
- **SWポイント選択のスイッチパネル対応** — スイッチパネルに「SWレイヤーヘッダー + 配下のSWポイント一覧」を表示。
|
||
クリックでSWポイントを選択(ダブルクリックでリネーム)、選択中ポイントのgroupsリストを展開表示
|
||
- `_mkSwLayerEl()` — SWレイヤー行DOM生成メソッド
|
||
- `_createSwLayer()` / `_deleteSwLayer()` — SWレイヤーの追加・削除
|
||
|
||
### Changed
|
||
- **`drawRigOverlay()`** — シグネチャ `swData={}` → `swLayers=[], selectedSwPointInfo=null`。
|
||
SWポイント描画ループを `swLayers[].points[]` に変更。選択判定を `selectedSwPointInfo` で行う
|
||
- **`hitTestRig()`** — シグネチャ `swData={}` → `swLayers=[]`。
|
||
戻り値のSWポイントヒットが `{layerId, type}` → `{swLayerId, swPointId, type}` に変更
|
||
- **`renderLayersToCtx()`** — SWによるカスタムグループ表示切り替えを `config.sw_layers` に対応
|
||
- **`_setupRigInteraction()`** — setupモードでのSW配置フローを変更:
|
||
SWレイヤー選択 + SWボタン + キャンバスクリック → `_placeSwPoint(wx, wy)` 呼び出し
|
||
- **`_placeSwPoint()`** — シグネチャ `(layerId, wx, wy, entry)` → `(wx, wy)`。
|
||
`_selectedSwLayerId` のSWレイヤーへ絶対座標でポイントを追加後、SWボタンを解除
|
||
- **`_renderSwitchTab()`** — swLayers構造でSWレイヤー/SWポイントを階層表示
|
||
- **`_addSwGroup()` / `_removeSwGroup()`** — `_selectedSwPointInfo` を使ってgroupsを操作
|
||
- **ノードキャンバス (`drawNodeCanvas`)** — `config.sw` → `config.sw_layers` に対応。
|
||
SWハンドルドラッグ時のpivot計算をlayerEntry依存から絶対座標に変更
|
||
|
||
---
|
||
|
||
## v2.12.0 — 2026-06-04
|
||
|
||
### Overview
|
||
Three additions: (1) background color picker and local image file input on the node canvas;
|
||
(2) optional `background_image` IMAGE input port for compositing an upstream node's image
|
||
as the background layer; (3) fix to SW point name inline-rename (double-click was broken
|
||
by the row click handler re-building the DOM before the second click registered).
|
||
|
||
### Fixed
|
||
- **SW point name rename broken** — `row.addEventListener("click")` called
|
||
`_renderSwitchTab()` on every click, rebuilding the DOM before the `dblclick` event on
|
||
`nameEl` could fire; fixed with a 250 ms timer pattern (same as the layer tree rename):
|
||
- `nameEl` click: start a 250 ms timer → on expiry, toggle selection and re-render
|
||
- `nameEl` dblclick: cancel the timer, keep `_selectedSwLayerId` set, open inline input
|
||
- `row` click handler skips when `e.target === nameEl` (nameEl owns its own selection logic)
|
||
|
||
### Added
|
||
- **Background color picker** — `BG` label + `<input type="color">` + `✕` clear button
|
||
added below the Point Size slider in the node canvas widget; selecting a color sets
|
||
`node._bgColorEnabled = true` and `node._bgColor`; clears local background image if set;
|
||
`✕` sets `_bgColorEnabled = false`; canvas redraws immediately
|
||
- **Background image (local file)** — `🖼 画像` button + hidden `<input type="file"
|
||
accept="image/*">` + `✕` clear button; file is read with `FileReader.readAsDataURL()` and
|
||
stored as an `Image` object in `node._bgImage`; button label updates to the filename (up
|
||
to 12 chars); selecting a file clears `_bgColorEnabled`; `✕` sets `_bgImage = null`
|
||
- **Background image (ComfyUI node input)** — optional `("IMAGE",)` input port
|
||
`background_image` added to `PSDLoaderNode.INPUT_TYPES`; when connected, the upstream
|
||
image is composited as the bottommost layer (letterbox resize, aspect-ratio preserved) via
|
||
the new `_composite_on_bg()` helper; applied in both the `image_data` (Capture) path and
|
||
the server-side compositing path
|
||
- **External background indicator** — when `background_image` input has an active link,
|
||
`onConnectionsChange` hook shows `🔗 外部接続中` label in the BG control row; canvas
|
||
displays a dark-green tinted placeholder with text
|
||
`"🔗 外部背景画像接続中 / Queue Prompt で反映されます"` instead of the checker pattern
|
||
|
||
### Changed
|
||
- **`drawNodeCanvas()` background priority** — checker pattern is now the last fallback;
|
||
priority: local image (`_bgImage`) → local color (`_bgColorEnabled`) →
|
||
external connection indicator → checker
|
||
- **`captureNode()` background** — same priority order applied to the temp canvas before
|
||
camera-transformed PSD layers are drawn; local background is baked into the captured PNG
|
||
- **`psd_loader_node.py` — `_composite_on_bg()` helper** — module-level helper function
|
||
encapsulates background compositing (tensor → PIL → letterbox resize → alpha_composite);
|
||
called in both the `image_data` branch and the server-side branch; `background_image` is
|
||
now applied even when `image_data` (Capture) is present, so the composited result always
|
||
includes the upstream background
|
||
- **`IS_CHANGED`** — includes `str(background_image.shape)` in the hash key so the node
|
||
re-executes when the upstream image dimensions or content changes
|
||
- **`PREVIEW_WIDGET_H`** — `PREVIEW_IMG_H + 62` (484 px) → `PREVIEW_IMG_H + 88` (510 px)
|
||
to accommodate the new BG control row (+26 px); `UI_WIDGET_H` 544 → 570 px
|
||
|
||
---
|
||
|
||
## v2.11.0 — 2026-06-04
|
||
|
||
### Overview
|
||
Two improvements: (1) bug fix — rig points were invisible after loading a model from the
|
||
Library or from the Setup modal's `📂 model` button; (2) new SW (Switch) point type that
|
||
toggles the visibility of registered custom groups by rotating a handle, allowing multi-state
|
||
layer switching (e.g. facial expression sets) without manual eye-toggle clicks.
|
||
|
||
### Fixed
|
||
- **Rig points not shown after Library model load** — `_loadModel()` set
|
||
`node._layerImages = null` then called `refreshNodePreview()`, which fell into the
|
||
`_compositeImg` branch of `drawNodeCanvas()` (no `_layerImages` → no rig overlay);
|
||
fixed by awaiting `loadLayerImages()` directly inside `_loadModel()` and calling
|
||
`drawNodeCanvas()` after `_layerImages` is populated
|
||
- **Rig points not shown after Setup modal model load** — `_reloadFromNode()` called
|
||
`_initPreview()` → `_drawPreview()` while `_rigMode` was still `'normal'` (rig overlay
|
||
guard: `if (this._rigMode !== 'normal')`), so points were invisible until the user
|
||
manually clicked Pose/Setup; fixed by checking `config.rigging` keys after state rebuild:
|
||
if rigging exists and `_rigMode === 'normal'`, automatically switch to `'pose'` mode and
|
||
call `_updateRigModeUI()` before the preview draw
|
||
|
||
### Added
|
||
- **SW (Switch) point type** — new rig point that controls group visibility via angle:
|
||
- **Visual**: green origin dot connected to a cyan handle by a green line; handle displays
|
||
the active group index; selection ring (yellow) on origin in setup mode
|
||
- **Setup arc overlay** — when the layer is selected in setup mode, a dashed green arc
|
||
shows the full angular range (`30° × n` groups), with tick marks at each 30° step
|
||
- **Placement**: select the SW button in the setup bar, pick a layer, click canvas to
|
||
place the origin; drag the cyan handle to set radius and initial angle
|
||
- **Pose mode**: dragging the cyan handle changes `swInfo.angle`; no radius change in
|
||
pose mode (radius is setup-only)
|
||
- **Group switching logic** (`renderLayersToCtx`): for each SW point, computes
|
||
`activeIdx = floor(normalizedAngle / 30°)` where `normalizedAngle = angle mod (30°×n)`;
|
||
the active group is unhidden (even if `visible:false`), all other registered groups are
|
||
hidden; SW override takes priority over `cg.visible` but not over unregistered groups
|
||
- **Maximum 12 groups** per SW point (12 × 30° = 360°)
|
||
|
||
- **SW button in Setup bar** — green-tinted `SW` button added to the right of `MR`;
|
||
sets `_setupPointType = 'sw'`; highlighted with a green outline when active
|
||
|
||
- **Switch tab** — third tab added to the right panel (レイヤー | ペアレント | **スイッチ**):
|
||
- Lists all SW points by name; click to select; double-click to rename inline; ✕ to delete
|
||
- Selected SW expands to show its registered groups as rows with `angle°` label and a
|
||
dropdown selector (shows all custom groups)
|
||
- `+` button appends a new group slot (dropdown defaults to first CG); `−` removes the
|
||
selected slot; limit 12 slots per SW
|
||
|
||
- **`_nextSwName()`** — generates unique auto-incremented names (`sw1`, `sw2`, …)
|
||
- **`_placeSwPoint()`** — places or moves the SW origin for the selected layer
|
||
- **`_renderSwitchTab()`** — renders the switch tab list and group slot rows
|
||
- **`_addSwGroup()` / `_removeSwGroup()`** — add or remove a group slot from the selected SW
|
||
|
||
### Changed
|
||
- **`LayerState`** — added `sw = {}` property; included in `toConfig()` as `sw:` key and
|
||
restored in `fromConfig()` from `config.sw`
|
||
- **`drawRigOverlay()`** — added `swData = {}` as the last parameter; draws SW points after
|
||
all R/MR points; call sites in `drawNodeCanvas()` and `PSDModal._drawPreview()` updated to
|
||
pass `config.sw || {}`; early-return guard expanded to also allow `swData` entries
|
||
- **`hitTestRig()`** — added `swData = {}` as the last parameter; tests cyan handle
|
||
(`sw_handle`) in both modes, and origin (`sw_origin`) in setup mode only; returns
|
||
`{ layerId, type: 'sw_handle' | 'sw_origin' }`; all call sites updated
|
||
- **`_setupRigInteraction()` (modal)**:
|
||
- Setup mousedown: SW button active → `_placeSwPoint()` on canvas miss; hit on
|
||
`sw_handle` or `sw_origin` → `setup_drag`; mousemove handles `sw_handle` (radius +
|
||
angle) and `sw_origin` (translate origin)
|
||
- Pose mousedown: `sw_handle` hit → `sw_pose_drag` drag info with pivot at SW origin
|
||
- Pose mousemove: `sw_pose_drag` → `swInfo.angle = atan2(wy−pivotY, wx−pivotX)`
|
||
- **`setupPreviewInteraction()` (node canvas)**:
|
||
- Mousedown: `sw_handle` hit → `isRigDrag = true`, `rigDragInfo` with SW pivot stored
|
||
- Mousemove: `hit.type === 'sw_handle'` branch updates `swInfo.angle` then writes
|
||
updated config to the widget and redraws; `config.rigging` access guarded with `|| {}`
|
||
- Hit-test guard changed from `if (config.rigging)` to `if (config.rigging || config.sw)`
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
{
|
||
"sw": {
|
||
"layerId": {
|
||
"name": "sw1", // display name (auto-incremented)
|
||
"x": 100, // origin X in layer-local coords
|
||
"y": 200, // origin Y in layer-local coords
|
||
"radius": 80, // distance to cyan handle (setup-editable)
|
||
"angle": 0, // current angle in radians (pose-editable)
|
||
"groups": ["cg_a", "cg_b"] // registered CG IDs; index 0 = 0°, 1 = 30°, …
|
||
}
|
||
}
|
||
}
|
||
```
|
||
Field is optional; configs without `sw` behave identically to prior versions.
|
||
|
||
---
|
||
|
||
## v2.10.0 — 2026-06-03
|
||
|
||
### Overview
|
||
Pose thumbnail display and save accuracy improvements. The Library pose grid now renders
|
||
thumbnails at the correct aspect ratio and responds correctly to S/M/L size switching.
|
||
Setup modal pose save now captures from the modal's own live preview instead of the
|
||
(potentially stale) node canvas. A green dashed frame overlay is always visible in the
|
||
Setup modal preview to mark the output crop region.
|
||
|
||
### Fixed
|
||
- **Pose thumbnail grid layout** — added `align-items:center` to each pose card
|
||
(`flex-direction:column`); without it, `thumbBox` stretched to fill the grid-cell width
|
||
while height stayed at `px`, causing a non-square distorted frame. S/M/L switching now
|
||
correctly resizes both the grid columns and each card's thumb container
|
||
- **Pose thumbnail aspect ratio on save** — changed fixed 70×44 canvas to
|
||
`140 × round(140 * fh/fw)` so the saved thumbnail matches the `output_width / output_height`
|
||
aspect ratio rather than a hard-coded 16:10 approximation (applies to both node pose-save
|
||
button and Setup modal pose-save button)
|
||
|
||
### Added
|
||
- **Output-frame overlay in Setup modal preview** — a semi-transparent green dashed border
|
||
(`rgba(0,220,80,0.65)`) drawn after `ctx.restore()` in `_drawPreview()` marks the output
|
||
crop region at all times, consistent with the node canvas overlay
|
||
- **`_computeModalOutputFrame()` method** — calculates the output-frame rectangle in screen
|
||
coordinates of `this._previewCanvas`, taking the current camera zoom/pan into account;
|
||
shared by `_drawPreview()` overlay and `_savePoseFromModal()` capture
|
||
|
||
### Changed
|
||
- **Setup modal pose save captures from `this._previewCanvas`** — `_savePoseFromModal()`
|
||
now uses `_computeModalOutputFrame()` to crop from the live modal preview instead of
|
||
`node._nodeCanvas` (which reflects the last Queue Prompt output, not the current pose
|
||
state); falls back to `node._nodeCanvas` when PSD metadata is not yet loaded
|
||
|
||
---
|
||
|
||
## v2.9.0 — 2026-06-03
|
||
|
||
### Overview
|
||
Library modal improvements: pose thumbnail aspect-ratio fix, S/M/L thumbnail size toggle,
|
||
selected-model thumbnail preview in the left panel, and model save now includes a 140×140
|
||
thumbnail. Setup modal title renamed. PSD/model file loading now refreshes the modal's
|
||
layer tree and state in-place. Setup can open without a PSD loaded.
|
||
|
||
### Fixed
|
||
- **Pose thumbnail aspect ratio** — replaced the fixed 70×44 `<canvas>` with an `<img>`
|
||
element sized `max-width:${px}px; max-height:${px}px; width:auto; height:auto;` inside a
|
||
square container, so the original output-frame aspect ratio is preserved at all thumbnail
|
||
sizes
|
||
- **Setup modal opens without PSD** — removed the `alert("先にPSDファイルを選択してください")`
|
||
guard in `openLayerModal()`; the call site now passes `layers || []` to `PSDModal` so an
|
||
empty tree is valid
|
||
|
||
### Added
|
||
- **Pose thumbnail S/M/L toggle** — three-button group added to the right-panel toolbar
|
||
(`S` = 76 px, `M` = 110 px, `L` = 160 px); selected size highlights; `_thumbPx()` helper
|
||
drives both the card container size and `grid-template-columns` via `_updateGridCss()`
|
||
- **Selected-model thumbnail panel** — 106 px area at the bottom of the left panel;
|
||
single-click on a model list item selects it (highlights row, shows thumbnail);
|
||
double-click loads the model onto the node and closes the modal
|
||
- **Model save includes thumbnail** — `_saveConfig()` renders a 140×140 frame from the
|
||
output region of the node canvas and stores it as `content.thumbnail` alongside
|
||
`psd_filename` and `layer_config`
|
||
- **`library_list_models` returns thumbnail** — server response now includes
|
||
`"thumbnail": data.get("thumbnail")` so the library can display model thumbnails without
|
||
a separate fetch
|
||
- **Setup modal file bar** — `📂 psd` / `📂 model` / `⟳` bar always visible above the
|
||
setup/pose sub-bars; opening a PSD or model file calls `_reloadFromNode()` to rebuild
|
||
`layerTree`, `state`, and re-render the tree and preview in place without reopening the
|
||
modal
|
||
- **`_reloadFromNode()` method** — rebuilds `this.layerTree`, `this.state`, clears
|
||
selection state, then calls `_renderTree()` and `_initPreview()`
|
||
|
||
### Changed
|
||
- **Setup modal title** — `"PSD レイヤー設定"` → `"PSD モデルエディタ"`
|
||
- **Model list interaction** — single-click selects (shows thumbnail); double-click loads;
|
||
right-click deletes with confirmation (previously single-click loaded immediately)
|
||
|
||
---
|
||
|
||
## v2.8.0 — 2026-06-03
|
||
|
||
### Overview
|
||
Library modal implementation and UI restructuring. Model/pose save now writes to
|
||
`user_data/models/` and `user_data/poses/` on the server instead of triggering browser
|
||
downloads. PSD file buttons moved from node row 1 into the Setup modal header area.
|
||
Node shrinks from 3 rows to 2 rows. Library modal supports browsing, loading, and deleting
|
||
saved models and poses.
|
||
|
||
### Added
|
||
- **Library API — server (`server.py`)** — six new aiohttp routes under `/psd_loader/library/`:
|
||
- `GET /models` — list all `.psd-model.json` files in `user_data/models/`
|
||
- `GET /models/{name}` — fetch a single model file
|
||
- `POST /models` — save model content to `user_data/models/<name>.psd-model.json`
|
||
- `DELETE /models/{name}` — delete a model file
|
||
- `GET /poses`, `GET /poses/{name}`, `POST /poses`, `DELETE /poses/{name}` — same CRUD
|
||
for `.pose.json` files in `user_data/poses/`
|
||
- All routes validate filenames against path-traversal characters (`..`, `/`, `\`)
|
||
- **Library modal (`LibraryModal` class)** — opens via the lib button (row 1 of node):
|
||
- Left panel: model search input + model list; click to load model onto node, right-click
|
||
to delete with confirmation
|
||
- Right panel: pose search input + thumbnail grid; click to apply pose to node, right-click
|
||
to delete; thumbnails use `<img>` tags so original aspect ratio is preserved
|
||
- Header `×` button and overlay click both close the modal
|
||
- **Setup modal — file bar** — always-visible bar inserted between the modal header and the
|
||
setup/pose sub-bars:
|
||
- `📂 psd` — opens file picker to upload/select a PSD (button label updates to filename)
|
||
- `📂 model` — opens file picker to load a `.psd-model.json` file directly
|
||
- `⟳` — refreshes preview
|
||
- **Setup modal — Pose button** — `📷 ポーズ` button added to the footer (between `💾 保存`
|
||
and `適用`); prompts for a name and saves current `visibility` + `pose` + 70×44 thumbnail
|
||
to `user_data/poses/` via `_savePoseFromModal()`
|
||
|
||
### Changed
|
||
- **Model save** (`_saveConfig()`) — no longer triggers a browser download; now prompts for
|
||
a name and POSTs `{ psd_filename, layer_config }` to `/psd_loader/library/models`
|
||
- **Pose snapshot button** — left-click now prompts for a name and POSTs to
|
||
`/psd_loader/library/poses`; thumbnail on the button icon does not update after save
|
||
(initial human-silhouette icon is preserved); right-click still applies the last-saved
|
||
pose from `node._poseSnapshots[0]`
|
||
- **Pose snapshot icon** — initial canvas draws a human-silhouette icon on a dark background
|
||
instead of the previous `+` text
|
||
- **Library button icon** — canvas replaced with a 3×2 coloured-block grid on a dark background
|
||
- **Node row layout — 3 rows → 2 rows**:
|
||
- Row 1 (psd / psd model / ⟳) removed; those buttons now live in the Setup modal file bar
|
||
- Remaining rows: `[lib] [pose] [Setup] [🏷] [RP] [RC]` and `[📸 Capture]`
|
||
- `UI_WIDGET_H` updated to `BTN_ROW_H × 2 + PREVIEW_WIDGET_H` = 544 px
|
||
- **Setup modal opens without PSD** — removed the `alert("先にPSDファイルを選択してください")`
|
||
guard so the modal can open with an empty layer tree; PSD can then be loaded via the
|
||
in-modal file bar
|
||
|
||
---
|
||
|
||
## v2.7.0 — 2026-06-03
|
||
|
||
### Overview
|
||
Four improvements: (1) camera roll now correctly applies to captured output; (2) rig point
|
||
size and label size are now adjustable with a slider on both the node canvas and the Editor
|
||
modal; (3) model files (`.psd-model.json`) can be saved from the Editor and loaded back onto
|
||
the node; (4) the node button layout is redesigned, and the pose snapshot button gains full
|
||
save / apply functionality.
|
||
|
||
### Fixed
|
||
- **Camera roll not reflected in capture output** — `captureNode()` applied
|
||
`translate → scale → translate` but omitted `rotate(cam.roll)`, so captured images
|
||
were always axis-aligned regardless of viewport roll; added `tmpCtx.rotate(cam.roll || 0)`
|
||
between the two translate calls, matching the transform sequence in `drawNodeCanvas()`
|
||
|
||
### Added
|
||
- **Point Size slider — node canvas** — `buildPreviewWidget()` appends a slider row
|
||
(`range 0.5–3.0, step 0.1, default 1.0`) below the status label; value stored as
|
||
`node._rigPointSize`; triggers `drawNodeCanvas()` on `input`
|
||
- **Point Size slider — Editor modal** — `previewPanel` receives the same slider between
|
||
`_previewStatusEl` and the RP button; value stored as `this._rigPointSize`; triggers
|
||
`_drawPreview()` on `input`
|
||
- **`drawRigOverlay` point size scaling** — signature gains `pointSize = 1.0` (last
|
||
parameter); three internal size variables computed at entry:
|
||
- `PR = round(7 × pointSize)` — rig point dot radius
|
||
- `SR = round(12 × pointSize)` — setup-mode selection ring radius
|
||
- `PSR = round(11 × pointSize)` — pose-mode selection ring radius
|
||
- All `_drawRigPoint`, `ctx.arc`, and flip-indicator font sizes use these variables;
|
||
radius label font scales as `max(8, round(10 × pointSize))px`
|
||
- **`_drawRigLabel` label size scaling** — gains `pointSize = 1.0` parameter; font size
|
||
`max(8, round(11 × pointSize))px`; background rect and vertical offset scale proportionally
|
||
- **`hitTestRig` point size scaling** — gains `pointSize = 1.0` parameter; hit radius
|
||
scales as `14 / zoom × pointSize` so click targets match visual point size
|
||
- **All `drawRigOverlay` / `hitTestRig` call sites** updated to pass the relevant
|
||
`node._rigPointSize` or `this._rigPointSize` value
|
||
- **Model file save — Editor modal** — `💾 保存` button added to the modal footer (between
|
||
キャンセル and 適用); `_saveConfig()` method serializes
|
||
`{ psd_filename, layer_config }` (including `layer_order`) to a
|
||
`<basename>.psd-model.json` file and triggers a browser download
|
||
- **Model file load — node** — `📂 psd model` button added to row 1 (between psd and ⟳);
|
||
opens a JSON file picker; on load, writes `layer_config` to the widget, updates
|
||
`psd_filename`, fetches layer tree from server, resets camera, and refreshes preview
|
||
- **Pose snapshot button — full implementation** — replaces the placeholder gradient;
|
||
initial state shows a `+` icon on a dark canvas:
|
||
- **Left-click → save**: captures `visibility` and `pose` from `layer_config`; renders
|
||
a 36 × 22 px thumbnail from the output frame region of the node canvas; stores as
|
||
`node._poseSnapshots[0]`; updates the button canvas; flashes green outline (600 ms)
|
||
- **Right-click → apply**: writes the saved `visibility` and `pose` back to
|
||
`layer_config` and calls `drawNodeCanvas()`
|
||
- Thumbnail image loaded via `Image` object into the button canvas to handle async
|
||
PNG decode correctly (`_refreshPoseSnapThumb` helper)
|
||
|
||
### Changed
|
||
- **Node button layout redesigned**:
|
||
- **Row 1**: `[📂 psd (flex:1)] [📂 psd model (flex:1)] [⟳ (28px)]`
|
||
- **Row 2**: `[lib PH (40px)] [pose snap (40px)] [Setup (flex:1)] [🏷] [RP] [RC]`
|
||
- **Row 3**: `[📸 Capture]`
|
||
- `Editor` button renamed to `Setup`
|
||
- Library button remains a gradient-thumbnail placeholder (disabled) for the upcoming
|
||
library modal
|
||
- **`PREVIEW_WIDGET_H`** recalculated from actual DOM measurements:
|
||
`PREVIEW_IMG_H + 62` = 484 px (wrap padding 10 + border 2 + status 18 + slider 26 +
|
||
margin buffer 6); `UI_WIDGET_H` updated to 574 px
|
||
|
||
---
|
||
|
||
## v2.6.1 — 2026-06-02
|
||
|
||
### Fixed
|
||
- **Parent tab: CG structure must not affect parent-child (rig) relationships** — the layer
|
||
tab shows display order / custom group structure; the parent tab shows only user-set
|
||
`layerParent` relationships between PSD layers; these two must be completely independent:
|
||
- Moving a layer into a CG auto-sets `layerParent[lid] = cgId` for rig transform
|
||
propagation, which was causing layers to disappear from the parent tab (they had a
|
||
parent, but the CG parent was not displayed) and corrupting existing rig hierarchies
|
||
- **`_ptAllOrderedIds()`** — rewritten to walk the PSD layer tree directly, completely
|
||
ignoring `cgOrder` and CG `layer_ids`; the parent tab display order is now based solely
|
||
on the original PSD structure, not on layer tab grouping
|
||
- **`_ptDeriveRoots()`** — filter changed to `!lp[id] || cgIds.has(lp[id])`: layers
|
||
whose `layerParent` points to a CG are treated as roots (the CG-assigned parent is
|
||
invisible to the parent tab)
|
||
- **`_ptDeriveChildren(parentId)`** — returns `[]` immediately when `parentId` is a CG
|
||
ID, so CG nodes can never function as rig parents in the parent tab hierarchy
|
||
|
||
---
|
||
|
||
## v2.6.0 — 2026-06-02
|
||
|
||
### Overview
|
||
Three UX improvements to the rig / Editor workflow: (1) layer flip via Alt / Ctrl click on rig
|
||
points; (2) right-drag panning in Editor setup mode; (3) collapsible hierarchy in the parent tab.
|
||
|
||
### Added
|
||
- **Layer flip in Pose mode** — Alt + click or Ctrl + click on any R or MR rig point
|
||
(blue or red) toggles a per-layer flip transform:
|
||
- **Alt + click** → toggle `pose[layerId].flipX` (horizontal mirror)
|
||
- **Ctrl + click** → toggle `pose[layerId].flipY` (vertical mirror)
|
||
- Both modifiers can be active simultaneously (`flipX && flipY` = 180° rotational symmetry)
|
||
- Flip pivot is the R point (falls back to MR if no R); applied **after** rotation and
|
||
**before** translation in `applyRigTransform` via `ctx.scale(±1, ±1)` around the pivot
|
||
- Works in both the Editor modal (`_setupRigInteraction`) and the node canvas
|
||
(`setupPreviewInteraction`); Alt+right-drag (camera roll) is unaffected because roll
|
||
requires button 2
|
||
- **`applyChainToPoint`** — flip added after rotation step:
|
||
`if (p.flipX) x = 2*px - x; if (p.flipY) y = 2*py - y`
|
||
- **`inverseChainToPoint`** — inverse flip inserted between translation undo and rotation
|
||
undo (flip is self-inverse: same formula)
|
||
- **`getAncestorChain` / `buildAncestorChain`** — chain-inclusion condition extended to
|
||
`|| p.flipX || p.flipY` so flipped ancestors are included even when angle/tx/ty are zero
|
||
- **`hasTf`** in `renderLayersToCtx` render loop — extended with `|| p.flipX || p.flipY`
|
||
for both the group branch and the leaf branch
|
||
- **Flip indicator in `drawRigOverlay`** — `↔` (flipX), `↕` (flipY), or `↔↕` (both)
|
||
drawn in yellow (`rgba(255,220,0,0.95)`) below the pivot point, visible in all modes
|
||
|
||
### Fixed
|
||
- **Editor setup mode: panning blocked when layer selected** — any click that missed an
|
||
existing rig point immediately called `_placeSetupPoint()`, making it impossible to pan
|
||
the preview canvas while a layer was selected; fixed by checking `e.button === 2` at the
|
||
top of the setup branch and entering `isPanning` state for right-button presses, leaving
|
||
left-click for point placement as before
|
||
|
||
### Changed
|
||
- **`_renderParentTab` — collapsible hierarchy** — parent tab now supports collapse /
|
||
expand using the existing `collapsedIds` set:
|
||
- Nodes with at least one `layerParent` child show a `▶ / ▼` toggle button; clicking it
|
||
hides or reveals the subtree without affecting other tabs
|
||
- Custom group nodes (`cg_xxx`) that appear as parents in `layerParent` are now resolved
|
||
via `this.state.customGroups` and displayed as `📂 [CG] name`; previously they were
|
||
silently skipped because `_findNodeById` only searches the PSD layer tree
|
||
- Clicking a CG row in the parent tab selects it (sets `_selectedCgId`), matching the
|
||
behavior of the layer tab
|
||
- Eye button and inline rename (PSD layers only via dblclick) preserved
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
{
|
||
"pose": {
|
||
"layerId": {
|
||
"flipX": boolean, // optional; horizontal flip around R/MR pivot
|
||
"flipY": boolean // optional; vertical flip around R/MR pivot
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## v2.4.0 — 2026-06-02
|
||
|
||
### Overview
|
||
Two improvements: (1) custom groups can now have rig points assigned directly, and those
|
||
points propagate transforms to member layers via `layer_parent`; (2) the Editor modal preview
|
||
now shows a PSD canvas boundary line matching the node canvas.
|
||
Includes fixes for orange-handle drag not working in Editor pose mode, and group rig not
|
||
moving member layers on the node canvas.
|
||
|
||
### Added
|
||
- **Custom group rigging** — custom groups can now be selected in Setup mode and have R / MR
|
||
rig points placed on them; the group acts as a virtual layer with `left:0, top:0` and PSD
|
||
canvas dimensions (`width:psdW, height:psdH`)
|
||
- **`getEffectiveImageMap(node, config)` (global helper)** — returns `node._layerImages`
|
||
merged with a virtual entry `{img:null, isCgLayer:true, left:0, top:0, width, height}`
|
||
for each custom group that has no existing entry; used by all rendering and interaction
|
||
paths on the node canvas
|
||
- **`PSDModal._getEffectiveImageMap()`** — same logic scoped to the modal's `this.state`
|
||
- **`drawRigOverlay` / `hitTestRig`** gain a `customGroups = []` parameter; any custom
|
||
group whose ID appears in `rigging` is appended as a virtual node `{id, name}` to
|
||
`allLayers` so its rig points are drawn and hittable
|
||
- **`_getSelectedLayerId()`** updated to return `this._selectedCgId` first so a selected
|
||
custom group is recognized as the Setup target
|
||
- **`_deleteSelectedRig()`** updated to delete the rig of `_selectedCgId` when a custom
|
||
group is selected
|
||
- **PSD canvas boundary in Editor modal** — a yellow dashed border (`rgba(255,255,100,0.5)`,
|
||
`1/zoom` px wide) is drawn around `(0, 0, psdW, psdH)` inside the camera transform block
|
||
in `_drawPreview()`, matching the node canvas boundary style
|
||
|
||
### Changed
|
||
- **`_createGroup()`** — after creating the group, automatically sets
|
||
`this.state.layerParent[lid] = cgId` for every member layer that has no existing
|
||
`layerParent` entry; also sets `this._selectedCgId = cgId` so the new group is immediately
|
||
selected and ready for rig point placement
|
||
- **`_moveItemToGroup()`** — when a layer is dragged into a group, sets
|
||
`layerParent[itemId] = groupId` if the item is not a CG and has no existing parent;
|
||
when it is removed from its previous group, clears `layerParent[itemId]` if that group
|
||
was its parent
|
||
- **`_removeItemFromGroup()`** — clears `layerParent[itemId]` when the removed item's
|
||
recorded parent matches the group being left
|
||
- **`_ungroup()`** — now also deletes `rigging[targetId]`, `pose[targetId]`, and clears
|
||
`layerParent` entries that pointed at the dissolved group
|
||
- **All rendering / interaction calls** updated to use effective image maps and pass
|
||
`customGroups`:
|
||
- `drawNodeCanvas()` — uses `getEffectiveImageMap`; passes `_config.custom_groups` to
|
||
`drawRigOverlay`
|
||
- `captureNode()` — uses `getEffectiveImageMap` (fixes group rig missing in output image)
|
||
- `setupPreviewInteraction` mousedown / mousemove — uses `getEffectiveImageMap`; passes
|
||
`config.custom_groups` to `hitTestRig`
|
||
- `_setupRigInteraction` (modal) mousedown / mousemove — uses `_getEffectiveImageMap()`
|
||
for entry lookup and `buildAncestorChain` call; passes `this.state.customGroups` to
|
||
`hitTestRig`
|
||
- `_drawPreview()` — uses `_getEffectiveImageMap()`; passes `this.state.customGroups` to
|
||
`drawRigOverlay`
|
||
|
||
### Fixed
|
||
- **Editor pose mode: orange handle not draggable** — `pose_drag` in `_setupRigInteraction`
|
||
used `const entry = node._layerImages[layerId]` directly; for custom-group rig IDs this
|
||
returned `undefined`, causing a TypeError that silently prevented `p.tx/ty` from updating;
|
||
fixed by using `this._getEffectiveImageMap()[layerId]`; same fix applied to the
|
||
`buildAncestorChain` call inside the orange branch
|
||
- **Node canvas: group rig not moving member layers** — `_moveItemToGroup` did not update
|
||
`layerParent`, so layers added to a group via drag-and-drop after group creation had no
|
||
ancestor chain; `renderLayersToCtx → getAncestorChain` therefore returned an empty chain
|
||
and the group's `tx/ty` was never propagated; fixed by auto-setting `layerParent` in
|
||
`_moveItemToGroup` (same guard used in `_createGroup`: only if not already set)
|
||
|
||
---
|
||
|
||
## v2.3.0 — 2026-06-02
|
||
|
||
### Overview
|
||
Camera roll on the node canvas preview: Alt + right-drag rotates the viewport around the
|
||
canvas center. All existing camera-transformed overlays (PSD boundary, rig points) correctly
|
||
follow the roll.
|
||
|
||
### Added
|
||
- **Camera roll** — `node._camera.roll` (radians, default `0`) applied as
|
||
`ctx.rotate(cam.roll)` between `ctx.translate(W/2, H/2)` and `ctx.scale(cam.zoom)` in
|
||
`drawNodeCanvas()`, so the entire PSD + rig + boundary rotates around the canvas center
|
||
- **Alt + right-drag interaction** — `setupPreviewInteraction()` detects `e.altKey && e.button === 2`
|
||
on `mousedown`; enters `isRolling` state; `mousemove` sets
|
||
`cam.roll = rollStartRoll + dx * 0.005` (≈ 0.29°/px); drag cursor `ew-resize`; roll
|
||
cleared on `mouseup` / `mouseleave` exit
|
||
- **`toWorld()` roll inverse** — canvas → PSD coordinate conversion in
|
||
`setupPreviewInteraction` now applies `cos(-roll) / sin(-roll)` rotation to the
|
||
canvas-center-relative pixel offset before dividing by zoom, so rig point hit-testing
|
||
remains accurate at any roll angle
|
||
- **`setDefaultCamera()`** — initializes `roll: 0` in both `node._camera` and
|
||
`node._defaultCamera`; camera reset (`RC` button) therefore also resets roll
|
||
- **PSD boundary overlay moved inside ctx transform block** — previously drawn after
|
||
`ctx.restore()` using `worldToCanvas` (which ignored roll); now drawn as
|
||
`ctx.strokeRect(0, 0, psdW, psdH)` inside the camera save/restore block with
|
||
`lineWidth = 1/cam.zoom` and `setLineDash([4/cam.zoom, 4/cam.zoom])`, so the boundary
|
||
rotates correctly with the viewport
|
||
|
||
---
|
||
|
||
## v2.2.0 — 2026-06-01
|
||
|
||
### Overview
|
||
Three groups of improvements: (1) rig points on the node canvas now correctly follow
|
||
`layerParent` ancestor transforms (matching the modal's existing behavior); (2) the node
|
||
button row gains 🏷 label-toggle and RP reset-pose buttons, and the Editor modal gets an
|
||
RP button below its preview; (3) the MR orange handle direction is now freely orientable
|
||
during setup instead of being fixed to the right — stored as a new `mr_angle` field.
|
||
|
||
### Fixed
|
||
- **Node canvas: R/MR points did not follow parent-layer transforms** — `drawRigOverlay`,
|
||
`hitTestRig`, and the orange-handle drag handler were all called without `layer_parent`,
|
||
so ancestor transforms were ignored on the node canvas:
|
||
- `drawNodeCanvas()` now passes `_config.layer_parent || {}`, `_config.renamed || {}`,
|
||
and `node._showRigLabels` to `drawRigOverlay`
|
||
- `setupPreviewInteraction` mousedown now passes `config.layer_parent || {}` to
|
||
`hitTestRig`; pivot position for `r` and `mr` types computed via
|
||
`buildAncestorChain + applyChainToPoint` (same logic as the modal)
|
||
- Orange-handle drag (`hit.type === 'orange'`) now applies `inverseChainToPoint` before
|
||
computing `tx/ty`, so dragging respects the ancestor rotation axes; `chain` is stored
|
||
in `rigDragInfo` at mousedown and reused in mousemove
|
||
|
||
### Added
|
||
- **`🏷` label toggle button on node row 2** — `node._showRigLabels` (default `false`);
|
||
green outline when active; triggers `drawNodeCanvas(node)`; placed between Editor and RC
|
||
- **`RP` button on node row 2** — calls `resetNodePose(node)`, which sets every rigged
|
||
layer's pose to `{angle:0, tx:0, ty:0}`, writes back to `layer_config`, and redraws;
|
||
placed between 🏷 and RC so the row reads `[Editor] [🏷] [RP] [RC]`
|
||
- **`RP` button in Editor modal preview panel** — `modalRpBtn` appended below
|
||
`_previewStatusEl`; resets `this.state.pose[id]` for every id in `this.state.rigging`
|
||
and calls `_drawPreview()`; available in all rig modes (normal / setup / pose)
|
||
- **`resetNodePose(node)`** — new top-level helper function (added after `resetNodeCamera`)
|
||
|
||
### Changed
|
||
- **MR orange handle: freely orientable direction** — previously the orange handle was
|
||
always displayed at `(mrOrigin.x + mr_radius, mrOrigin.y)` (fixed rightward) during
|
||
setup; now it is positioned at angle `mr_angle` from the MR origin:
|
||
- **`drawRigOverlay` setup branch** — `oX/oY` computed as
|
||
`(sX + r·cos(mr_angle), sY + r·sin(mr_angle))` where `mr_angle` defaults to `0`;
|
||
connecting dashed line and radius label follow the new `oY`
|
||
- **`hitTestRig` setup branch** — orange hit target moved to the same angled position
|
||
- **`_setupRigInteraction` setup_drag** — orange drag now updates both `mr_radius` (`hypot`)
|
||
and `mr_angle` (`atan2`) from the drag delta, allowing the user to drag the handle in
|
||
any direction
|
||
- **`_placeSetupPoint()`** — when `mr_radius === 0` at first MR placement, initializes
|
||
`mr_angle = 0` alongside `mr_radius = 50`
|
||
|
||
### Schema addition
|
||
```jsonc
|
||
{
|
||
"rigging": {
|
||
"layerId": {
|
||
"mr_angle": number // radians; 0 = rightward (default). New optional field.
|
||
}
|
||
}
|
||
}
|
||
```
|
||
Existing configs without `mr_angle` default to `0` (unchanged visual behavior for right-pointing handles).
|
||
|
||
---
|
||
|
||
## v2.1.0 — 2026-06-01
|
||
|
||
### Overview
|
||
UI refactor separating display order from parent-child relationships into two tabs, plus a
|
||
critical fix ensuring that `layerParent` rig-transform propagation no longer alters the
|
||
rendering order, and full visual/interaction support for rig points on child layers.
|
||
|
||
### Added
|
||
- **レイヤー / ペアレント タブ** — the right panel is now split into two tabs:
|
||
- **レイヤータブ**: shows `cgOrder` display order with drag-and-drop, custom group
|
||
create/ungroup buttons; `layerParent` indentation is not shown here
|
||
- **ペアレントタブ**: shows `layerParent` parent-child hierarchy; buttons `▲ ▼ ◀ ▶`;
|
||
order here is purely visual and has no effect on rendering
|
||
- **`parentTabOrder`** — new `LayerState` field (`parent_tab_order` in JSON) storing
|
||
`{ roots, children }` for the parent tab's independent sibling order; `null` entries
|
||
fall back to `cgOrder`-derived order; persisted via `toConfig`/`fromConfig`
|
||
- **`▲ ▼` buttons (parent tab)** — `_shiftParentTabItem(dir)` swaps a layer with its
|
||
previous/next sibling in `parentTabOrder` without touching `cgOrder`
|
||
- **Tab-aware `_indent` / `_outdent`** — dispatches to `_indentParent` / `_outdentParent`
|
||
(which update `parentTabOrder`) when the parent tab is active; falls back to the
|
||
original `cgOrder`-based logic in the layer tab
|
||
- **Standalone rig-transform helpers** — `buildAncestorChain`, `applyChainToPoint`,
|
||
`inverseChainToPoint` added as top-level functions for use by overlay and hit-test code
|
||
|
||
### Fixed
|
||
- **Rendering order preserved after `layerParent` assignment** — `renderLayersToCtx`
|
||
no longer skips child layers in the main render pass; `drawLayerTree` (which reordered
|
||
children after their parent) is removed and replaced by `getAncestorChain`, which
|
||
accumulates parent rig transforms and applies them via `ctx.save/restore` to each child
|
||
at its original `cgOrder` position — display order is fully determined by `cgOrder`, not
|
||
by `layerParent`
|
||
- **Child rig points shown at correct visual position** — `drawRigOverlay` now accepts
|
||
`layerParentMap` and applies each ancestor's `ctx` transform (rotation + translation)
|
||
in a `save/restore` block before drawing the layer's rig points; points for child layers
|
||
move correctly when a parent is posed
|
||
- **Child rig point hit detection corrected** — `hitTestRig` accepts `layerParentMap` and
|
||
calls `applyChainToPoint` to convert each rig point from local to visual coordinates
|
||
before distance comparison; clicking on a visually-moved rig point now registers correctly
|
||
- **Pose drag pivot uses visual coordinates** — when starting a pose drag on a child layer,
|
||
`buildAncestorChain` + `applyChainToPoint` compute the pivot's visual position; angle
|
||
delta is measured from the visual pivot, giving correct rotation behavior
|
||
- **MR orange-handle drag corrected for rotated ancestors** — `inverseChainToPoint` converts
|
||
the drag position from visual (world) space back to the layer's ancestor-local space before
|
||
computing `tx/ty`; motion direction now matches the ancestor's rotated axes
|
||
|
||
### Schema additions
|
||
```jsonc
|
||
{
|
||
"parent_tab_order": { "roots": ["id", …] | null, "children": { "parentId": ["childId", …] } }
|
||
}
|
||
```
|
||
Field is optional; omitting it derives order from `cgOrder`.
|
||
|
||
---
|
||
|
||
## v2.0.0 — 2026-06-01
|
||
|
||
### Overview
|
||
Major structural additions: (1) PSD group layers can now carry R/MR rig points; (2) custom
|
||
groups can be nested arbitrarily; (3) a unified `cgOrder` list controls the display order of
|
||
custom groups and PSD layers together; (4) `◀ ▶` indent buttons enable parent-child
|
||
relationships between layers without creating a custom group; (5) `layerParent` propagates
|
||
parent rig transforms to child layers hierarchically (tail-chain use case).
|
||
|
||
### Added
|
||
- **Group layer rigging** — `loadLayerImages` now creates a pseudo-entry `{img:null, isGroup:true,
|
||
left, top, width, height}` for every PSD group node; `hitTestRig`, `drawRigOverlay`, and
|
||
`_placeSetupPoint` handle groups automatically; `renderLayersToCtx` applies the group's
|
||
`rig/pose` transform to a `ctx.save/restore` block wrapping all its children
|
||
- **Nested custom groups** — `layer_ids` may now contain CG IDs (prefixed `cg_`); `renderCg()`
|
||
is recursive; `_moveItemToGroup()` has a descendant-set cycle check before moving; parent
|
||
visibility propagates recursively via `markHidden()`; `_mkCustomGroupEl` is draggable
|
||
(`dragstart` / `drop`) so groups can be rearranged or nested by drag-and-drop
|
||
- **`cgOrder`** — new `LayerState` field (and `cg_order` in saved JSON) storing the top-level
|
||
display sequence of root CG IDs and PSD root layer IDs (upper-to-lower order); `_renderTree`
|
||
iterates `cgOrder` instead of separate CG-then-PSD passes, allowing groups and PSD layers to
|
||
be freely interleaved; `_reorder()` updates `cgOrder` when both endpoints are at the root level;
|
||
migration: configs without `cg_order` reconstruct the order as `[rootCgIds…, reversedPsdIds…]`
|
||
- **`◀ ▶` indent buttons** — added to the group bar (right of グループ解除); behaviour:
|
||
- **▶ (indent)**: sets `layerParent[selId] = prevId` where `prevId` is the immediately
|
||
preceding item in the same scope; if `prevId` is a CG the operation is a no-op (does not
|
||
enter CGs); works inside custom groups without escaping them
|
||
- **◀ (outdent)**: for layers — deletes `layerParent[selId]` (stays within its CG scope);
|
||
for CGs — removes from parent CG and re-inserts into `cgOrder` / grandparent `layer_ids`
|
||
at the parent's position + 1
|
||
- **`layerParent`** — new `LayerState` field (and `layer_parent` in saved JSON) mapping
|
||
`childId → parentId` for layer-level parent-child relationships:
|
||
- `_renderTree` computes a depth-first `displayOrder` per scope and indents children;
|
||
only root items (no parent in same scope) are rendered at the top of each list;
|
||
`collapsedIds` suppresses children
|
||
- `renderLayersToCtx` builds a reverse map `layerChildrenMap`; `drawLayerTree(n, entry)`
|
||
applies the layer's own `rig/pose` transform in a `ctx.save/restore` block, then recurses
|
||
into children — child layers inherit the accumulated canvas transform from all ancestors;
|
||
layers that have a `layerParent` entry are skipped in the main `render()` pass to prevent
|
||
double-drawing
|
||
- `applyRigTransform(entry, rig, p)` helper extracted to eliminate duplicated pivot /
|
||
rotation / translation code across the group and layer branches
|
||
|
||
### Changed
|
||
- **`_createGroup()`** — if `_selectedCgId` is set, the new CG is appended to that group's
|
||
`layer_ids` (sub-group); otherwise it is `unshift`-ed into `cgOrder` (root group)
|
||
- **`_ungroup()`** — removes the target CG from `cgOrder` and from all parent `layer_ids`;
|
||
child CGs are promoted to `cgOrder` at the removed CG's former index position
|
||
- **`_moveItemToGroup()`** — removes the item from `cgOrder` when it transitions from root CG
|
||
to nested CG; `_removeItemFromGroup()` re-inserts it into `cgOrder` when promoted back to root
|
||
|
||
### Schema additions
|
||
```jsonc
|
||
{
|
||
"cg_order": ["cg_xxx", "psd_layer_id", …], // root display order (new)
|
||
"layer_parent": { "child_id": "parent_id", … } // layer hierarchy (new)
|
||
}
|
||
```
|
||
Both fields are optional; configs without them fall back to prior behaviour.
|
||
|
||
---
|
||
|
||
## v1.7.0 — 2026-05-30
|
||
|
||
### Overview
|
||
MR point system redesign. R and MR points are now strictly independent: placing R creates
|
||
only R (blue), placing MR creates only MR (red) — the previous auto-creation of both is
|
||
removed. MR now supports both rotation (drag red point) and translation (drag orange point)
|
||
in pose mode, with the orange handle and red dashed line visible in both setup and pose modes.
|
||
Visual style updated to match the particle renderer reference (red dashed line connecting
|
||
MR origin to orange radius handle).
|
||
|
||
### Changed
|
||
- **`_placeSetupPoint()`** — initialization refactored:
|
||
- Previous: first placement always created `{ r:{…}, mr:{…}, mr_radius:0 }` regardless of
|
||
which button was active
|
||
- New: `if (!rig)` creates `{ mr_radius: 0 }` only; R button adds `rig.r` if absent; MR
|
||
button adds `rig.mr` if absent — the two point types no longer coerce each other
|
||
- Default `mr_radius = 50` is applied only when `mr_radius === 0` at MR placement time
|
||
- **`renderLayersToCtx()`** — rotation pivot logic generalized:
|
||
- Previous: only `rig.r` was used as rotation pivot; MR-only layers could not rotate
|
||
- New: pivot = `rig.r ?? rig.mr`; MR-only layers now rotate around `rig.mr`
|
||
- **`drawRigOverlay()` — pose mode branch** rewritten for MR:
|
||
- MR origin (red dot) drawn at fixed world position `(entry.left + mr.x, entry.top + mr.y)`;
|
||
no `tx/ty` applied, as MR is the rotation anchor
|
||
- Orange dot drawn at `(MR_world + p.tx, MR_world + p.ty)` — tracks current translation
|
||
- Red dashed line `[4,3]` / `rgba(220,50,50,0.85)` / 2px connects MR origin to orange dot
|
||
- Orange range circle `[5,5]` / `rgba(255,136,0,0.4)` drawn around MR origin at `mr_radius`
|
||
- All MR visuals (circle, line, orange dot) conditional on `mr_radius > 0`
|
||
- **`drawRigOverlay()` — setup mode branch** — connecting line color changed:
|
||
- Previous: orange `rgba(255,136,0,0.7)`
|
||
- New: red `rgba(220,50,50,0.85)` / `[4,3]` / 2px, matching pose mode
|
||
- Radius label `R: {px}` added in orange next to the orange handle
|
||
- **`hitTestRig()` — pose mode branch** — MR and orange tested independently:
|
||
- MR origin hit at fixed `(entry.left + mr.x, entry.top + mr.y)` (no tx/ty)
|
||
- Orange hit at `(MR_world.x + tx, MR_world.y + ty)` — only when `mr_radius > 0`
|
||
- Orange tested before MR so it takes priority when positions overlap (tx=ty=0)
|
||
- **Pose drag — modal (`_setupRigInteraction`) and node canvas (`setupPreviewInteraction`)**:
|
||
- `hit.type === 'r'` → rotation around R (unchanged)
|
||
- `hit.type === 'mr'` → rotation around fixed MR origin (`pivotX/Y = entry + mr.x/y`, no tx)
|
||
- `hit.type === 'orange'` → translation: `p.tx = wx − mr.x`, `p.ty = wy − mr.y`, clamped to
|
||
`mr_radius` circle (logic moved from former `'mr'` branch)
|
||
- Both drag-start blocks (`mousedown`) compute `pivotX/Y` and `initAngle` for `'mr'` type
|
||
|
||
### Notes
|
||
- A layer can now have `r` only, `mr` only, or both; existing saved configs with both keys
|
||
continue to work (R used for rotation if present, otherwise MR; MR orange for translation)
|
||
- Multiple points per layer (numbered) is deferred to a future version
|
||
|
||
---
|
||
|
||
## v1.6.0 — 2026-05-30
|
||
|
||
### Overview
|
||
Three improvements to the rigging/posing workflow: (1) the modal preview panel is enlarged
|
||
1.5× and gains the same pan/zoom camera as the node canvas; (2) rig points now display layer
|
||
name labels so nearby or overlapping points can be identified; (3) the rig point model is
|
||
corrected — R and MR are now fully independent systems, data keys renamed throughout.
|
||
|
||
### Added
|
||
- **Modal preview pan/zoom** — preview panel width `260 → 390 px`; modal width `720 → 850 px`;
|
||
canvas internal resolution fixed at `366 × 520`; camera (`_previewCam: {x, y, zoom}`) set
|
||
at open time to fit-zoom the PSD, same algorithm as the node canvas
|
||
- Left-drag → pan (normal, setup-miss, and pose-miss all fall through to pan)
|
||
- Mouse wheel → zoom around cursor (factor 1.12, range 0.05×–20×)
|
||
- `_setDefaultPreviewCam()` helper recomputes fit-zoom from current PSD dimensions
|
||
- **Rig point labels** — `_drawRigLabel(ctx, x, y, label, isSelected)` helper renders a
|
||
semi-transparent black background + white text (yellow when selected) above each rig point
|
||
- `drawRigOverlay()` gains `showLabels = false` and `renamed = {}` parameters
|
||
- Setup mode: always shown; label positioned above the R or MR point respectively
|
||
- Pose mode: controlled by `_showRigLabels` flag (default `true`); a **🏷 ラベル** toggle
|
||
button appears in the green pose bar (highlighted with green outline when active)
|
||
- **Pose mode bar** — `poseBar` element (green tint) shown below header when Pose mode is
|
||
active; currently contains only the label toggle button; symmetric with `setupBar`
|
||
|
||
### Changed
|
||
- **Rig point model — R and MR are now independent**
|
||
- `pivot` → `r` (Rotation): rotation-only point; posing rotates the layer around `r`
|
||
- `stretch_origin` → `mr`, `stretch_radius` → `mr_radius` (Move and Rotation):
|
||
translation-only point; `mr` is origin, orange handle sets `mr_radius`; posing drags the
|
||
layer within the radius circle; `p.tx / p.ty` are updated
|
||
- The two point types are designed for exclusive use per layer (one or the other); both can
|
||
coexist but each operates independently
|
||
- **`renderLayersToCtx()`** — rewritten to handle R-only, MR-only, or both:
|
||
- R present + `p.angle`: applies `translate(px,py) rotate(angle) translate(-px,-py)`
|
||
- `p.tx || p.ty`: applies `translate(tx, ty)` (from MR interaction)
|
||
- No rig or no non-zero pose values: draws as before
|
||
- **`drawRigOverlay()`** — rewritten per-point-type loop:
|
||
- Guard changed from `if (!rig?.r)` to `if (!rig?.r && !rig?.mr)` so MR-only layers render
|
||
- R block (blue dot) and MR block (red dot + orange radius handle) drawn independently
|
||
- Selected-layer yellow ring follows the active point (R preferred over MR when both exist)
|
||
- **`hitTestRig()`** — same guard fix; setup and pose branches each test R and MR independently
|
||
- **`PSDModal._buildDOM()`** — rig button labels: `"🔵 Pivot"` → `"R"`, `"🔴 Stretch"` → `"MR"`;
|
||
internal names `_btnPivot/_btnStretch` → `_btnR/_btnMR`; `_setupPointType` default `'pivot'` → `'r'`
|
||
- **`_drawPreview()`** — migrated from calling global `drawPreview()` (which reset canvas size
|
||
to PSD dimensions) to inline camera-transformed rendering matching node canvas approach;
|
||
rig overlay now rendered inside the `ctx.save/restore` camera block
|
||
- **`_setupRigInteraction()`** — completely rewritten; previously lacked pan/zoom; coordinate
|
||
transform `toPsd()` now inverts camera matrix; `hitTestRig` receives `_previewCam.zoom`
|
||
- **Rig drag start (modal + node)** — `pivotX/Y` and `initAngle` computed only when
|
||
`hit.type === 'r'` so MR-only layers no longer throw when `rig.r` is absent
|
||
- **`_placeSetupPoint()` and `setup_drag` init** — data object key `pivot:` → `r:`
|
||
|
||
### Schema change
|
||
```
|
||
rigging: {
|
||
layerId: {
|
||
r: { x: number, y: number }, // was pivot
|
||
mr: { x: number, y: number }, // was stretch_origin
|
||
mr_radius: number // was stretch_radius
|
||
}
|
||
}
|
||
```
|
||
Existing saved workflows using the old keys will stop applying rig transforms (the keys will
|
||
simply be absent); re-place the rig points in Setup mode to migrate.
|
||
|
||
---
|
||
|
||
## v1.5.0 — 2026-05-30
|
||
|
||
### Overview
|
||
Layer rigging system. Two point types can be placed on each layer: a **pivot point** (blue)
|
||
that acts as the rotation centre, and a **stretch point** (red) that translates the layer
|
||
within a configurable radius. Setup is done in the Editor modal; posing works both in the
|
||
modal and directly on the node canvas (same interaction model as the 2D Pose Editor reference).
|
||
|
||
### Added
|
||
- **Rigging data in `layer_config`** — two new top-level keys:
|
||
- `rigging`: `{ layerId: { pivot:{x,y}, stretch_origin:{x,y}, stretch_radius:number } }`
|
||
— coordinates are in layer-local space (offset from `entry.left / entry.top`)
|
||
- `pose`: `{ layerId: { angle:number, tx:number, ty:number } }`
|
||
— `angle` = rotation around pivot (radians); `tx/ty` = translation from stretch interaction
|
||
- **`drawRigOverlay(ctx, layers, imageMap, rigging, pose, mode, selectedLayerId, setupPointType)`**
|
||
— draws rig points on a canvas context; adapts to `'setup'` vs `'pose'` mode:
|
||
- Setup: raw (un-posed) coordinates; shows orange handle + dashed radius circle + blue pivot + red stretch origin; highlights active point type with a ring
|
||
- Pose: posed coordinates (pivot + tx/ty offset); blue pivot dot + red stretch dot
|
||
- **`hitTestRig(wx, wy, …)`** — returns `{ layerId, type:'pivot'|'stretch'|'orange' }` for the
|
||
nearest rig point within a zoom-adjusted 14 px hit radius; used by both modal and node canvas
|
||
- **`_drawRigPoint(ctx, x, y, r, fill, stroke)`** — small helper for filled circle with stroke
|
||
- **Modal Setup mode** — toggled by [Setup] button in modal header:
|
||
- Sub-bar appears with [🔵 Pivot] / [🔴 Stretch] type selector + [🗑 削除] delete button
|
||
- Click on preview canvas → places selected point type at cursor position in layer-local coords
|
||
- Drag existing point → repositions it
|
||
- Drag orange handle → updates `stretch_radius`
|
||
- **Modal Pose mode** — toggled by [Pose] button:
|
||
- Drag blue pivot dot → updates `pose[id].angle` via `atan2(mouse − pivot) − atan2(start − pivot)`
|
||
- Drag red stretch dot → updates `pose[id].tx/ty`; clamped to `stretch_radius` circle
|
||
- **Node canvas rig interaction** — `setupPreviewInteraction()` now hit-tests rig points on
|
||
`mousedown`; if a point is hit, enters rig-drag mode (same pivot/stretch logic as modal pose);
|
||
if no point hit, falls through to existing pan behaviour; updates `layer_config` widget on
|
||
every `mousemove` so `drawNodeCanvas()` shows live feedback
|
||
- **`node._rigSelectedLayerId`** — tracks which layer is highlighted (yellow ring) on the node
|
||
canvas rig overlay
|
||
|
||
### Changed
|
||
- **`LayerState`** — added `rigging` and `pose` properties; both included in `toConfig()` and
|
||
restored in `fromConfig()`
|
||
- **`renderLayersToCtx()`** — when a layer has both a `rigging` entry (with `pivot`) and a
|
||
`pose` entry with non-zero values, applies `ctx.translate(tx, ty)` → `ctx.translate(pivotX, pivotY)`
|
||
→ `ctx.rotate(angle)` → `ctx.translate(-pivotX, -pivotY)` before `drawImage`; layers without
|
||
rig data draw as before
|
||
- **`drawNodeCanvas()`** — calls `drawRigOverlay()` in pose mode after rendering layers;
|
||
config object extracted to a named variable so it can be reused for the overlay call
|
||
|
||
### Fixed
|
||
- **Layer name rename double-click not firing** — `row.addEventListener("click")` called
|
||
`_renderTree()` which rebuilt the DOM before `dblclick` could fire on the original `nameEl`;
|
||
fixed by delaying the `_renderTree()` call 250 ms when the click target is `nameEl`; a
|
||
`dblclick` arriving within that window cancels the timer via `clearTimeout`
|
||
- **Custom group name rename double-click not firing** — same root cause as above; same fix
|
||
applied to `_mkCustomGroupEl`
|
||
- **Layer compositing order reversed** — psd-tools `enumerate(psd)` yields layers in
|
||
back-to-front order (`psd[0]` = bottommost); `_renderTree` already used `.reverse()` for
|
||
correct UI display (foreground at top), but `renderLayersToCtx` and `_manual_composite`
|
||
also called `.reverse()` / `reversed()`, making the draw order front-to-back (each layer
|
||
was painted *under* the previous one); removed all `reversed()` / `.reverse()` calls from
|
||
the three compositing paths (`renderLayersToCtx`, `_manual_composite`,
|
||
`_manual_composite_ordered`) so layers are drawn back-to-front as intended
|
||
|
||
---
|
||
|
||
## v1.4.0 — 2026-05-29
|
||
|
||
### Overview
|
||
Interactive preview canvas with pan/zoom/capture. The node preview is now a camera:
|
||
pan and zoom to frame the subject, then Capture to output exactly what is visible.
|
||
Output frame overlay shows the capture boundary based on output_width × output_height
|
||
aspect ratio. Capture triggers Queue Prompt automatically.
|
||
|
||
### Added
|
||
- **Interactive preview canvas** — replaced static `<img>` + placeholder with a `<canvas>`
|
||
element (462 × 422 px internal); serves as the interactive camera viewport
|
||
- **Pan** — left-click drag moves the canvas view; direction and scale correctly account for
|
||
CSS-to-canvas pixel ratio (`canvas.width / rect.width`) and zoom level
|
||
- **Zoom** — mouse wheel zooms around the cursor position (not canvas center); range 0.05×–20×
|
||
- **RC button** — resets camera to the fit-zoom state computed at PSD load time
|
||
(`zoom = min(462/psdW, 422/psdH) * 0.95`); placed alongside Editor button in row 2
|
||
- **Capture button** — captures current viewport as `image_data` and immediately calls
|
||
`app.queuePrompt(0, 1)` to run the node; button shows ⏳ Queuing… → ✅ Done! feedback
|
||
- **`image_data` widget** (backend) — added to `PSDLoaderNode.INPUT_TYPES` as optional STRING;
|
||
when non-empty the backend decodes base64 PNG → PIL → tensor, bypassing server compositing;
|
||
clears on workflow save via `app.graph.serialize` wrapper hook (same pattern as 2D Pose Editor)
|
||
- **`IS_CHANGED`** — added to `PSDLoaderNode`; hashes all parameters including `image_data`
|
||
so the node re-executes whenever capture data changes
|
||
- **Output frame overlay** — semi-transparent dark mask outside the capture boundary, white
|
||
border + L-corner markers + size label (`W × H`) at bottom-right; frame is the largest
|
||
rectangle inside the preview canvas that matches `output_width / output_height` aspect ratio
|
||
- **PSD boundary overlay** — yellow dashed border showing PSD content extents within the
|
||
viewport; rendered in world-space coordinates via camera transform inverse
|
||
- **`computeOutputFrame(node)`** helper — computes the crop rectangle `{fx, fy, fw, fh}`
|
||
from output_width/height aspect ratio and preview canvas dimensions
|
||
- **`worldToCanvas(wx, wy, cam, W, H)`** helper — converts PSD world coordinates to canvas
|
||
pixel coordinates under current camera state
|
||
- **Output widget callbacks** — `output_width` and `output_height` widget callbacks trigger
|
||
`drawNodeCanvas(node)` immediately so the output frame redraws on value change
|
||
|
||
### Changed
|
||
- **Capture → viewport crop** — capture renders the output frame region (`fw × fh` in preview
|
||
canvas space) mapped to `output_width × output_height`; transform:
|
||
`scale(cam.zoom * outW/fw)` centered at frame center (`outW/2, outH/2`); mathematically
|
||
equivalent to rendering the world region visible through the output frame rectangle
|
||
- **Node layout** — added third button row for Capture; `UI_WIDGET_H` updated:
|
||
`BTN_H(30) × 3 + PREVIEW_WIDGET_H(450) = 540`
|
||
- **`_setNodePreview()`** — requests preview image at `canvas.width * 2` for higher
|
||
resolution fallback when layer images are not yet cached
|
||
- **`PSDModal._apply()`** — calls `drawNodeCanvas(node)` instead of setting `img.src`
|
||
- **`refreshNodePreview()`** — calls `drawNodeCanvas(node)` directly when layer images cached
|
||
|
||
### Fixed
|
||
- **`ModuleNotFoundError: No module named psd_pose_editor`** — reverted `__init__.py` to
|
||
original (only imports `psd_loader_node`); `psd_pose_editor` subpackage is a reference/test
|
||
artifact and is not installed in the ComfyUI custom_nodes path
|
||
|
||
---
|
||
|
||
## v1.3.0 — 2026-05-28
|
||
|
||
### Overview
|
||
Resolved persistent node height / preview overflow issues caused by ComfyUI's DOM widget
|
||
placement system. Consolidated all custom UI elements into a single DOM widget to eliminate
|
||
multi-widget layout interference, and overrode `prototype.computeSize` to guarantee correct
|
||
node height regardless of ComfyUI's internal recalculation timing.
|
||
|
||
### Fixed
|
||
- **Preview overflows node frame (height too small)** — root cause: `computeSize()` called
|
||
before DOM widgets are fully initialized returns incorrect height; ComfyUI also periodically
|
||
re-calls `computeSize()` and resets `node.size[1]`, discarding manually set values
|
||
- Fix 1 (rejected): `requestAnimationFrame` delay — ComfyUI overwrites size again after
|
||
the callback fires
|
||
- Fix 2 (final): override `nodeType.prototype.computeSize` to always return a minimum
|
||
height of `UI_WIDGET_H + 80`, preventing ComfyUI from shrinking the node
|
||
- **`psd_btns` widget renders at full node height** — root cause: ComfyUI allocates a single
|
||
DOM overlay container for all DOM widgets; multiple DOM widgets (`psd_btns`, `psd_preview`)
|
||
caused placement interference where the first widget expanded to fill the entire container
|
||
- Fix: merged `psd_btns`, `Editor` button, and `psd_preview` into one DOM widget (`psd_ui`)
|
||
backed by a single `uiWrap` flex column container; ComfyUI now allocates exactly one
|
||
overlay region with no cross-widget interference
|
||
- **4 px preview clip at bottom** — `PREVIEW_WIDGET_H` was 446 but actual `previewWrap`
|
||
height is 450 (padding 10 + previewBox 422 + statusEl 18); corrected constant to match
|
||
|
||
### Changed
|
||
- `PREVIEW_WIDGET_H`: `PREVIEW_IMG_H + 24` (446) → `PREVIEW_IMG_H + 28` (450)
|
||
- `UI_WIDGET_H` new constant: `30 + 34 + PREVIEW_WIDGET_H` = 514
|
||
- `30` = btnRow, `34` = editorBtn (height 26 + margin 8), `450` = previewWrap
|
||
- `addDOMWidget("psd_btns")` and `addWidget("button", "Editor")` removed; replaced by
|
||
`editorBtn` HTML button and `btnRow` inside the unified `uiWrap` flex container
|
||
- `addDOMWidget("psd_preview")` replaced by `addDOMWidget("psd_ui")` (the unified container)
|
||
- `prototype.computeSize` override added in `beforeRegisterNodeDef` (before `onNodeCreated`)
|
||
with `minH = UI_WIDGET_H + 80`; `onNodeCreated` and `onConfigure` now set `node.size`
|
||
directly without `requestAnimationFrame`
|
||
|
||
---
|
||
|
||
## v1.2.0 — 2026-05-28
|
||
|
||
### Overview
|
||
Fixed preview area overflow, implemented fixed node size (474×733), and redesigned
|
||
preview widget with always-visible 422×422 area using inner-container isolation pattern.
|
||
Also fixed button stretching on wide nodes.
|
||
|
||
### Added
|
||
- **Fixed node size** (474×733) — `node.resizable = false` with `node.onResize` override
|
||
enforces constant size; follows the same pattern as the 2D Pose Editor reference node
|
||
- **Always-visible preview area** (422×422) — preview placeholder (checker pattern) shown
|
||
before PSD is loaded; replaced by the actual composite image after load
|
||
- **Checker placeholder canvas** — drawn programmatically as a 64×64 tiled checker,
|
||
displayed full-size via CSS `width:100%; height:100%` with `image-rendering:pixelated`
|
||
- **`PREVIEW_IMG_H = 422`** and **`PREVIEW_NODE_W = 474`** module-level constants
|
||
for centralized size management
|
||
|
||
### Fixed
|
||
- **Preview overflow / clipping to 29px** — root cause: `overflow:hidden` on the outer
|
||
DOM widget element (whose height ComfyUI sets incorrectly) clipped the preview content
|
||
- Fix: removed `overflow:hidden` from the outer `wrap`; added inner `previewBox` div
|
||
with explicit `height:422px` and `overflow:hidden`; placeholder and img use
|
||
`position:absolute; top:0; left:0; width:100%; height:100%` inside `previewBox`
|
||
- ComfyUI cannot affect `previewBox` height since it is a child element, not the
|
||
registered widget element
|
||
- **PSD load button stretching** — `psdBtn` with `flex:1` expanded to fill full node
|
||
width when node was wide (e.g., output_width=1024); fixed by adding `max-width:220px`
|
||
- **Node size not updating** — previous `setTimeout + if (node.size[0] < PREVIEW_NODE_W)`
|
||
conditional skipped height update when node was already wide; replaced with immediate
|
||
unconditional `node.size[0] = PREVIEW_NODE_W; node.size[1] = node.computeSize()[1]`
|
||
- **Saved workflow restoring wrong node size** — `onConfigure` now also applies
|
||
`node.size[0] = PREVIEW_NODE_W; node.size[1] = node.computeSize()[1]` to override
|
||
stale saved sizes
|
||
|
||
### Changed
|
||
- Preview widget `computeSize(w)` now returns fixed `[w, PREVIEW_IMG_H + 24]` (446px)
|
||
instead of dynamic `[w, node._previewH ?? 0]`
|
||
- Removed `_updateNodePreviewHeight()` function and all dynamic height calculation logic
|
||
- `buildPreviewWidget()` restructured: outer `wrap` (no overflow) → inner `previewBox`
|
||
(fixed 422px, overflow:hidden) → placeholder + img (absolute positioned)
|
||
- Simplified `refreshNodePreview`, `_apply`, and canvas path to remove height update calls
|
||
|
||
---
|
||
|
||
## v1.1.0 — 2026-05-28
|
||
|
||
### Overview
|
||
Added layer order reflection in output image, node preview display, output size controls,
|
||
and Editor layer order display matching Affinity Photo (foreground at top).
|
||
|
||
### Added
|
||
- **Node preview widget** — composite preview rendered inside the node using server-side
|
||
`/psd_loader/preview` API or client-side canvas (when layer images are cached)
|
||
- **`output_width` / `output_height` INT parameters** — resize output image; 0 = no resize;
|
||
aspect ratio preserved if only one dimension is set
|
||
- **Layer order in output** — `layer_order` key saved to `layer_config` JSON in `_apply()`;
|
||
`composite_with_config()` uses `_manual_composite_ordered()` when `layer_order` is present
|
||
- **`_manual_composite_ordered()`** in `psd_utils.py` — renders layers in the order specified
|
||
by the frontend layer tree (foreground-first list, reversed for back-to-front compositing)
|
||
- **Frontend layer reordering** — drag-and-drop reorder in the Editor modal; reordered tree
|
||
saved as `layer_order` in config on Apply
|
||
- **Foreground-first layer display** — Editor shows layers with foreground at top (reversed
|
||
from psd-tools' back-first array) using `[...nodes].reverse()` in `_renderTree`
|
||
- **Canvas-based node preview** — when `node._layerImages` is cached, `drawPreview()` renders
|
||
directly to a canvas and sets `img.src` as dataURL (no server round-trip)
|
||
|
||
### Fixed
|
||
- **Import path error** (`GET /extensions/scripts/app.js 404`) — changed from relative
|
||
`../../scripts/app.js` to absolute `/scripts/app.js`
|
||
- **"Extension already registered" error** — deleted duplicate `web/psd_loader.js` (web root);
|
||
active file is `web/js/psd_loader.js`
|
||
- **Files not reflected in ComfyUI** — created Windows Junction point linking development
|
||
directory to ComfyUI custom_nodes directory
|
||
|
||
### Changed
|
||
- `psd_utils.py`: added `_manual_composite_ordered()` and modified `composite_with_config()`
|
||
to use it when `layer_order` is present in config
|
||
- `psd_loader_node.py`: added `output_width`/`output_height` params and resize logic using
|
||
`Image.LANCZOS`
|
||
|
||
---
|
||
|
||
## v1.0.0 — (prior session)
|
||
|
||
### Overview
|
||
Initial implementation of PSD Loader ComfyUI custom node.
|
||
|
||
### Added
|
||
- PSD file upload via `/psd_loader/upload` API endpoint
|
||
- Layer tree parsing and display in modal Editor
|
||
- Layer visibility toggle with eye button
|
||
- Custom group creation (drag-and-drop layers into named groups)
|
||
- Layer visibility reflected in composite output
|
||
- Node preview via `/psd_loader/preview` API
|
||
- Layer rename (double-click inline edit)
|
||
- `get_layer_image_by_id()` for per-layer image fetch
|
||
- `composite_with_config()` with three fallback methods (layer_filter, visible override, manual)
|
||
- Server routes: `/psd_loader/upload`, `/psd_loader/layers`, `/psd_loader/layer_image`,
|
||
`/psd_loader/preview`
|