Auto-save the active render layer's sublayer file

Render layer overrides only reached disk on an explicit File > Save --
SaveDirtyRenderLayers() was called from SaveUsdFile/SaveUsdFileAs and nowhere
else. Everything a user authored after switching to a render layer (property
values, shader parameters, material assignments) sat in a dirty in-memory
SdfLayer marked with an orange asterisk, and a crash took the whole layer's
overrides with it.

Application::Update() now polls the active render layer's IsDirty() and saves
it 0.5s after the user goes idle, gated on IsAnyItemActive() and the left mouse
button so a slider or gizmo drag produces one write on release rather than one
per frame.

Polling rather than hooking CommandHistory::Push/Undo/Redo is deliberate: many
of the writes this is meant to catch never build a command and author straight
to the ambient edit target -- PropertyPanel's live WriteTranslate/Rotate/Scale
and its generic attribute widgets, TransformManipulator's gizmo drags, and
MaterialEditorPanel::RenderInputWidget's shader input writes. A command hook
would have silently missed exactly the property and shader-parameter edits at
issue. IsDirty() catches every path, commanded or not.

Scope is the active layer only. Muted render layers still rely on
SaveDirtyRenderLayers() at save time, and the root layer is never auto-written
-- so the persisted active-layer choice (custom layer data on the root layer)
still only survives a reopen once the stage itself is saved. Auto-writing the
user's main scene file was ruled out.

- SaveRenderLayerIfDirty() refreshes LayerManager after a successful save, or
  the cached isDirty snapshot leaves a stale asterisk in StageEditorPanel and
  the Scene Hierarchy sublayer list. A failed save latches the layer id so a
  read-only file can't spam the log twice a second.
- Switching layers flushes the outgoing one, and Shutdown() flushes before the
  panels are destroyed, so edits inside the debounce window aren't stranded.
- RefreshManagers() clears the auto-save state, whose layer ids belong to the
  outgoing stage.
- New "Auto-save active layer" checkbox in the Render Layer panel, on by
  default, persisted as AutoSaveRenderLayer in preferences.ini.

See docs/adr/0002-render-layer.md D9-D11 for the rationale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 09:47:58 +08:00
parent 05201465be
commit fac4ca0c74
5 changed files with 198 additions and 0 deletions
+73
View File
@@ -313,3 +313,76 @@ round-trip、跨圖層切換的 Undo/Redo 行為),寫法比照
`0001-viewport-color-correction.md` 文末的「Supersession Note」
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
決策當下的紀錄。
---
## 後續紀錄 — 現用 Render Layer 的自動存檔(2026-08-05)
- **Component:** `src/ui/Application``src/ui/RenderLayerPanel`
### Context
D8 讓 render layer 的內容只有在使用者明確存檔(File > Save /
Save As)時才會寫入磁碟——`Application::SaveDirtyRenderLayers()`
的呼叫點就只有 `SaveUsdFile()``SaveUsdFileAs()` 兩處。在那之前,
使用者切換到某個 render layer 後所做的所有編輯(屬性數值、shader
參數、材質指派)都只存在於記憶體中的 dirty `SdfLayer` 裡,
Render Layer 面板僅以橘色 `*` 標示;一旦當掉,該圖層的全部覆寫
就會遺失。使用者要求:切換到現用 render layer 之後,只要編輯屬性、
shader 參數或材質指派,該圖層的 sublayer 就應該自動存檔。
### D9 — 以「去彈跳的 dirty 輪詢」實作,而非掛在 `CommandHistory` 上
**決策:**`Application::Update()` 每一幀輪詢現用 render layer 的
`SdfLayer::IsDirty()`;當(a)沒有任何 ImGui item 處於 active、
且(b)滑鼠左鍵未按下、且(c)已維持 0.5 秒,才呼叫
`SdfLayer::Save()`。實作為
`Application::TickRenderLayerAutoSave()` /
`FlushActiveRenderLayerSave()` / `SaveRenderLayerIfDirty()`
**理由:** 規劃階段清查全部的寫入路徑後發現,使用者點名的編輯裡有
相當大一部分**根本不經過 `CommandHistory`**,而是直接對 ambient
edit target authoring:`PropertyPanel::WriteTranslate/WriteRotate/
WriteScale``PropertyPanel` 的一般屬性 widget 與 variant 選擇、
`TransformManipulator::ApplyTranslate/Rotate/Scale`(操作桿拖曳期間
的即時寫入)、以及 `MaterialEditorPanel::RenderInputWidget`
shader input 即時寫入。若把自動存檔掛在
`CommandHistory::Push/Undo/Redo`,這些路徑會被**靜默漏掉**,正好
涵蓋使用者最在意的「編輯屬性與 shader 參數」情境。輪詢
`IsDirty()` 則不管走不走 command 都能捕捉到。
(b) 的滑鼠條件是必要的:viewport 操作桿與 node editor 的拖曳是自行
hit-test 的,不是 ImGui item,`IsAnyItemActive()` 對它們回傳 false;
少了這個條件,拖曳期間會每幀寫檔一次。
### D10 — 只自動存現用的那一個 render layer
**決策:** 自動存檔只寫**現用**的 render layer;root layer、一般內容
sublayer、以及其餘被 mute 的 render layer 都維持原狀,仍需 Ctrl+S。
D8 完全不變、且仍然必要。
**已知取捨:** `PersistActiveLayerId()` 是把「目前哪個圖層生效」寫進
**root layer** 的 custom layer data,而 root layer 不在自動存檔範圍內
——因此「重新開檔後回到同一個現用圖層」這件事,仍然只有在使用者
明確存檔過之後才成立。刻意不自動寫 root layer:那等於在使用者沒有
要求的情況下改寫他的主場景檔案。
另有兩個附帶行為:切換圖層(或切回 Default)時會先把前一個圖層
flush 掉,避免在 0.5 秒去彈跳視窗內切走而漏存;`Shutdown()`
在面板被解構前也會 flush 一次。Undo/Redo 同樣會弄髒圖層、因而觸發
自動存檔,這是預期行為。
### D11 — 開關放在 Render Layer 面板,預設開啟
**決策:** `RenderLayerPanel` 提供「Auto-save active layer」
checkbox(預設勾選),狀態以 `AutoSaveRenderLayer` 鍵持久化到
`preferences.ini`。面板自己持有該旗標,`Application` 每幀讀回——
`MaterialEditorPanel` 的 column widths 採同一種
setter/getter 模式,不需要額外的 callback。
**理由:** 自動寫檔是行為變更,預設開啟才能滿足使用者的要求,但仍
保留關閉的退路。
**注意:** 沿用既有慣例,`preferences.ini` 只在 `Shutdown()` 時寫出,
所以這個開關的狀態在當掉時不會被保存——這是既有偏好設定共通的
行為,本次未一併變更。