diff --git a/docs/adr/0002-render-layer.md b/docs/adr/0002-render-layer.md new file mode 100644 index 0000000..0bf1833 --- /dev/null +++ b/docs/adr/0002-render-layer.md @@ -0,0 +1,315 @@ +# ADR 0002 — Render Layer 面板(Maya 風格,建構於 USD Sublayer 架構之上) + +- **Status:** Proposed(尚未實作) +- **Date:** 2026-07-12 +- **Component:** `src/core/RenderLayerManager`(新)、`src/ui/RenderLayerPanel`(新)、 + `src/core/commands/RenderLayerCommands`(新)、 + `src/core/commands/ConnectShaderAttrsCommand`、 + `src/core/commands/DisconnectShaderAttrCommand`、`src/ui/MaterialEditorPanel`、 + `src/core/UsdStageManager`、`src/ui/StageEditorPanel`、 + `src/ui/SceneHierarchyPanel`、`src/ui/Application` +- **Commit:** (尚未實作,無對應 commit) + +## Context + +使用者希望在本應用中加入一個「Render Layer」面板,行為比照 Maya 的 +Render Layers:使用者可以把場景中的燈光與可渲染物件加入某個具名圖層, +該圖層記錄對這些物件的屬性調整——包含 shading 連線的修改——而不影響 +場景的基礎資料;切換目前作用中的圖層時,viewport 套用該圖層的所有覆寫, +並且**只顯示該圖層的成員燈光與物件**;預設圖層(Default)則等同於目前 +未經任何 render layer 修改的 base stage。 + +為了先弄清楚該怎麼設計,再動手實作,我們先派出三個平行的研究/探索 +subagent:一個研究 Maya Render Layers(含舊版 Render Layers 與後續的 +Render Setup)的實際資料模型與行為語意;一個深入探索本專案既有的 +layer/stage/command 架構(`LayerManager`、`UsdStageManager`、 +`CommandHistory`、既有的 command 範本、`PropertyManager`、 +`StageEditorPanel`);一個探索 viewport 可見度控制與既有的材質覆寫機制 +(`UsdSceneRenderer`/`UsdImagingGLEngine`、`SceneHierarchyPanel` 的 +眼睛圖示隱藏機制、`MaterialEditorPanel` 的材質綁定與 shader 連線 +command)。三份研究都完成後,再交由一個 Plan agent 讀取實際原始碼, +驗證並修正整體設計——這個過程中抓到兩個既有程式碼裡的真實正確性問題 +(見下方 D3、D4)。本 ADR 記錄這輪研究與規劃後定案的設計決策。 + +**與真實 Maya 行為的刻意偏離:** Maya 切換 render layer 預設**不會** +自動 isolate viewport——那只是純粹的 render-time/override-time 概念, +非成員物件只有在該圖層明確加了 visibility override 時才會被隱藏。 +但本專案的使用者明確要求「切換到圖層時,只顯示該圖層內的燈光與物件」, +這是經與使用者確認過的刻意設計,而非誤解 Maya 行為。 + +## Goals / Non-Goals + +**Goals:** +- 非破壞性地記錄每個圖層的屬性覆寫(未來階段含 shading connection 與 + material binding 覆寫),不修改場景的基礎資料。 +- 切換作用中圖層時,viewport 即時反映該圖層的所有覆寫。 +- 切換作用中圖層時,viewport 只顯示該圖層的成員燈光/物件(isolate)。 +- 正確的存檔/重新開檔 round-trip(圖層清單、成員資格、覆寫內容、目前 + 作用中的圖層都要能還原)。 +- 圖層切換與圖層內的編輯都要能正確 Undo/Redo。 + +**Non-Goals(本階段,MVP 範圍之外,留待後續 Phase):** +- Shading connection 覆寫的使用者介面(底層機制在 Phase 0 就會修正到 + 可正確運作,但本階段不在 Render Layer 面板上開放這個操作)。 +- Material binding(材質指派)覆寫的使用者介面。 +- 每圖層覆寫清單的檢視/還原(revert)UI,比照 Maya Render Setup 的 + Property Editor 覆寫堆疊視圖。 +- 從 Scene Hierarchy 拖放物件到 Render Layer 面板做成員編輯(MVP 先用 + 「加入所選」按鈕取代)。 + +這些項目會在後續 Phase(見 Consequences 與另一份實作計畫 +`misty-watching-stardust.md`)補齊,此處先列出以留紀錄,避免日後誤以為 +被遺漏。 + +## Decision + +### D1 — 每個 Render Layer 對應一個獨立、檔案化的 `SdfLayer` + +**決策:** 每建立一個 render layer,就建立一個實體(檔案化,而非 +anonymous)的 `SdfLayer`,掛載為 stage root layer 的 sublayer,並以 +`SdfLayer::SetCustomLayerData` 標記 +(例如 `usdLayerManager:renderLayer = true`、 +`usdLayerManager:renderLayerName = "<名稱>"`),藉此與一般內容 sublayer +區分。絕對不可放進 session layer。 + +**理由:** `UsdStageManager::MergeSessionLayerIntoRoot()` 會在每次存檔 +時把 session layer 的內容攤平合併進 root layer、再清空 session +layer——若 render layer 的資料放在 session layer,存檔當下就會被摧毀。 +另外,anonymous `SdfLayer` 的 identifier(`anon:0x...`)只在執行期有效, +寫進 `subLayerPaths` 之後無法在 `SaveStageAs`/重新開檔時被正確解析, +因此每個 render layer 必須是真正的檔案(沿用 +`LayerManager::CreateNewSublayer` 既有的「建立檔案 → 存檔 → 插入 +sublayer」流程)。 + +--- + +### D2 — Default 圖層是虛擬的,沒有對應的 `SdfLayer` + +**決策:** 「Default」不是一個真正的 render layer 物件,選取它只代表 +「把所有 render-layer sublayer 全部靜音(mute)」。 + +**理由:** 這正好等同於今天應用程式既有的行為——沒有任何 render layer +生效時,viewport 顯示的就是未經修改的 base stage。不需要為 Default +另外設計一套資料結構或程式碼路徑。 + +--- + +### D3 — 成員資格以 `UsdCollectionAPI` 表示,並修正「非現用圖層無法讀寫」的問題 + +**決策:** 每個 render layer 的成員資格,用 `UsdCollectionAPI` +(instance name 固定為 `"members"`)表示,掛在該圖層自己 `SdfLayer` +內部一個固定保留路徑(`/RenderLayerData`)的 `over`-only admin prim +上。這個 prim 只有 `over`、沒有任何 `def`,所以是合法、可定址的 +`UsdPrim`,但 `IsDefined() == false`,不會出現在 +`UsdStage::Traverse()`、Scene Hierarchy 面板、或 +`PropertyManager::GetPrimPaths()` 的預設列舉結果中,不需要額外過濾。 + +規劃過程中發現一個關鍵問題:由於同一時間只有一個 render layer 是 +「現用」(unmuted)的,其餘全部處於 muted 狀態,而 +`UsdStage::MuteLayer` 會讓該圖層**完全**退出 stage 的合成 +(composition)——也就是說,對一個目前被 mute 的圖層呼叫 +`stage->GetPrimAtPath("/RenderLayerData")` 會拿到一個**無效**的 +`UsdPrim`,`Usd` 層級的 `UsdCollectionAPI` 便完全無法讀寫該圖層的成員 +資料(例如使用者想在 Render Layer 面板編輯一個目前非現用圖層的成員 +清單時)。 + +**修正做法:** 對於非現用圖層的成員資格讀寫,一律改走**Sdf 層級 API**, +直接對該圖層的 `SdfLayerHandle` 操作(`SdfCreatePrimInLayer` +建立/取得 `/RenderLayerData` 的 prim spec,再用 `SdfRelationshipSpec` +操作 `collection:members:includes` 這個 relationship 的 +target path 清單)——這條路徑完全不經過 stage 合成,因此不受 mute +狀態影響,而且寫入的 opinion 名稱與 `UsdCollectionAPI` 完全一致。 +只有在計算「目前現用圖層」的 isolate 用成員集合時,才使用一般 +`Usd` 層級 API(`UsdCollectionAPI::GetCollection` + +`ComputeMembershipQuery` + `UsdComputeIncludedObjectsFromCollection`), +因為此時該圖層必然已經是 unmuted 的。兩條路徑存取的是同一份 +authored opinion,只是兩種不同的入口,不是兩套 schema。 + +--- + +### D4 — 重用既有的 edit-target 機制做屬性覆寫,並修正三個既有的正確性缺陷 + +**決策:** 切換作用中的 render layer 時,把 stage 的 +ambient edit target 指向該圖層的 `SdfLayer`(沿用 +`LayerManager::SetEditTarget`)。 + +**理由與驗證:** 重新閱讀 `PropertyManager.cpp` 與 `TransformCommand` +後確認,`PropertyManager::SetPropertyValue`(Property Panel 一般屬性 +編輯的路徑)與 `TransformCommand`(viewport 操作桿與 Property Panel +的 Transform 編輯路徑)都是在編輯提交的當下,從 stage 目前的 +ambient edit target 解析要寫入哪個 `SdfLayer`。因此,只要把 ambient +edit target 換成 render layer 的 `SdfLayer`,這兩條既有的寫入路徑 +**完全不需要修改任何程式碼**,就會自動把編輯正確記錄到對應的 render +layer 裡。 + +但在驗證過程中,發現另外三個既有的寫入路徑目前**沒有**捕捉/套用 +`UsdEditContext`,而是在 `Execute()`/`Undo()` 真正執行的當下才讀取 +ambient edit target:`ConnectShaderAttrsCommand`、 +`DisconnectShaderAttrCommand`、以及 +`MaterialEditorPanel::BindMaterialToTarget` 的材質綁定/解除綁定 +closure。具體會出錯的情境:使用者在 Render Layer A 生效時連接了一條 +shader 連線(此時正確寫入 A);接著切換到 Render Layer B(ambient +edit target 也隨之換成 B);此時按下復原(Undo),`Undo()` 會在 +**B** 這個圖層上執行「移除連線」,而不是回到原本執行 `Execute()` +時的 A——結果是 A 留下一條沒被正確復原的連線 opinion,B 卻多了一條 +不該存在的覆寫。這其實是既有程式碼中已經存在、但目前很少被觸發到的 +潛在 bug(任何在「連線」與「復原連線」之間發生 edit target 變更的 +情境都會中招),必須在導入 render layer 之前先修正,否則問題會被 +render layer 的切換行為放大成使用者能輕易踩到的日常錯誤。 + +**修正做法:** 比照 `TransformCommand` 既有的寫法,為 +`ConnectShaderAttrsCommand`、`DisconnectShaderAttrCommand` +新增一個建構參數 `SdfLayerHandle editLayer`,並在 `Execute()`/ +`Undo()` 內以 `UsdEditContext(m_stage, m_editLayer)` +包裹實際的連線/解除連線呼叫;`MaterialEditorPanel` 在建立這些 +command 與綁定/解除綁定的 closure 時,一併在建構當下捕捉 +`stage->GetEditTarget().GetLayer()` 並傳入。這個修正不影響現有 +(尚無 render layer 時)的行為——沒有明確傳入圖層時,維持原本讀取 +ambient edit target 的行為不變。 + +--- + +### D5 — Viewport Isolate 採用「authored visibility opinion」,而非重建渲染引擎或整個 stage 的 population mask + +**決策:** 圖層啟用、或該圖層成員資格變動時,對場景做一次由上而下的 +遍歷:若某個子樹完全不含任何成員,就在該子樹**最上層**的 prim 上, +用 `UsdGeomImageable`(與 Scene Hierarchy 面板既有的眼睛圖示隱藏 +機制完全相同的原語)authoring 一個 `visibility = invisible`,並且 +**停止往下遞迴**(子孫節點靠繼承取得隱藏效果);若子樹內含成員,則不 +寫入任何東西、繼續往下遞迴。規則上永遠不主動 authoring +`visible`——只利用預設的「inherited」語意,避免蓋掉場景中原本就刻意 +隱藏的物件。所有自動寫入的路徑,記錄在該圖層自己的 +`customLayerData`(例如 `usdLayerManager:autoHiddenPaths`)中, +以便下次重新計算前可以精準只清除這些自動產生的 opinion,不誤刪 +使用者手動authoring 的覆寫。 + +**理由(排除的替代方案見下方 Alternatives Considered):** +重新閱讀 +`third_party/OpenUSD-v25.05/include/pxr/usdImagingGL/engine.h` +確認,`UsdImagingGLEngine::Parameters::invisedPaths` +只能在**建構時**指定,整個類別沒有任何執行期可用的 +setter——每次切換圖層都重建 engine 的代價太高(會丟失 Hydra +scene-index 狀態,以及 Arnold/Cycles/Embree 等漸進式渲染委派已累積 +的取樣結果,並造成 viewport 明顯的畫面閃爍)。而 +`UsdStage::SetPopulationMask()`/`SetLoadRules()` +雖然可在執行期呼叫,但作用範圍是整個 stage 的合成,會連帶讓 +Scene Hierarchy 面板、Property Panel 的選取、viewport 的 +pick 都看不到/選不到非成員物件——這遠超出「只讓 viewport +isolate」的需求,等於把物件從整個場景圖裡「拿掉」而非「隱藏」。 +以真正的 authored opinion 達成 isolate,則是可檢視、可除錯的 +USD 資料(能在 Stage Editor 等既有工具中直接看到),而且已經證實 +Hydra 能對這類編輯即時反應而不需要重建 engine(既有的 Property +Panel 編輯、Scene Hierarchy 的眼睛圖示隱藏,都是同一套機制)。 +開銷與被隱藏的「子樹根節點」數量成正比,而非與場景總物件數成正比, +對一般場景規模而言可忽略。 + +--- + +### D6 — 圖層切換本身也是一個可 Undo 的操作 + +**決策:** 新增 `RenderLayerActivateCommand`,和其他編輯一樣推入 +`CommandHistory`。建構時就先記錄「切換前的 edit target」,以便 +Undo 或切回 Default 時正確還原。 + +**理由:** 切換圖層會 authoring 真正的狀態(isolate 用的 visibility +opinion、edit target 的變更),如果不納入 Undo 堆疊,使用者連續按 +Ctrl+Z 時會「跳過」圖層切換這一步,卻仍然復原了在該圖層內做的編輯, +造成不一致、難以理解的復原歷史。此設計已與使用者確認。 + +--- + +### D7 — 圖層生效期間,鎖定既有的 edit-target 相關 UI + +**決策:** 當某個非 Default 的 render layer 是現用狀態時,停用 +Stage Editor 面板的 edit-target checkbox,以及 Scene Hierarchy +面板的圖層下拉選單,並加上提示文字說明原因。 + +**理由:** 這兩個既有的 UI 都是直接讀寫 stage 的 ambient edit +target。如果放任使用者在 render layer 生效期間透過這些既有 UI +把 edit target 改到別的地方,「編輯會正確落在目前作用中的 render +layer」這個核心不變量就會被悄悄破壞,且沒有任何提示。鎖定雖然犧牲 +一些彈性,但能避免使用者在不知情的狀況下把覆寫寫錯地方。此設計已 +與使用者確認(相對於「保留可操作、只顯示警告」的替代方案)。 + +--- + +### D8 — 存檔與重新開檔的持久化 + +**決策:** `UsdStageManager::SaveStage`/`SaveStageAs` +除了既有的存檔呼叫之外,額外明確逐一存下**每一個** render-layer +`SdfLayer`(不論目前是否被 mute)。哪個圖層目前是「現用」這件事, +額外以 root layer 的 custom layer data 記錄 +(例如 `usdLayerManager:activeRenderLayer`),並在 +`Application::RefreshManagers()`/開啟 stage 時讀回,重新套用對應的 +`MuteLayer`/`UnmuteLayer` 與 `SetEditTarget`。 + +**理由:** `UsdStage::Save()` +只會走訪目前在合成(composition)中的 layer 存檔,而同一時間必然有 +N-1 個 render-layer sublayer 處於 muted、不在合成中的狀態——若不 +額外處理,這些圖層的編輯內容在存檔時會被靜默遺漏。另外,mute 狀態 +本身只是 `UsdStage` 的執行期旗標,並不會被序列化進任何檔案,因此 +「目前是哪個圖層生效」這件事也必須另外顯式持久化,否則重新開檔後 +會遺失,退回成 Default 生效的狀態。 + +## Consequences + +**正面:** +- 幾乎完全重用既有的 `SdfLayer`/`UsdEditContext`/`CommandHistory`/ + `LayerManager` 機制,不需要引入新的渲染或合成機制,實作與既有 + 程式碼風格高度一致。 +- 每個圖層的覆寫都是真正、可檢視的 USD opinion,能在 Stage Editor + 等既有工具中直接檢視、除錯,不是一個只存在於應用程式記憶體裡的 + 「影子」狀態。 +- Isolate 邏輯的開銷與「被隱藏的子樹根節點數」成正比,不隨場景總 + 物件數線性增加。 +- 圖層切換、圖層內編輯,都能正確納入既有的 Undo/Redo 體系,使用者 + 操作心智模型一致。 + +**負面/取捨:** +- 成員資格需要維護兩條存取路徑(現用圖層走 `Usd` 層級 API、非現用 + 圖層走 `Sdf` 層級 API),增加一些實作複雜度與日後維護成本。 +- 鎖定既有 edit-target UI 犧牲了一部分操作彈性。 +- 每個 render layer 都對應一個實體檔案(而非 anonymous layer), + 會在專案目錄下產生額外的附屬檔案,需要一致的檔名/存放規則。 +- 本階段(MVP)尚未支援 shading connection、material binding 的 + 每圖層覆寫 UI,也還沒有覆寫清單的檢視/還原介面——這些留待後續 + Phase,在此之前 Render Layer 面板的覆寫能力僅限一般屬性數值與 + Transform。 +- `ConnectShaderAttrsCommand`/`DisconnectShaderAttrCommand`/ + `BindMaterialToTarget` 的修正屬於既有程式碼的行為調整,雖然設計上 + 刻意保持向下相容(未傳入圖層時行為不變),仍需要在導入 render + layer 之前完成並個別驗證,不能與 render layer 本身的功能驗證 + 混在一起。 + +## Alternatives Considered + +- **用 `UsdStage::SetPopulationMask()`/`SetLoadRules()` 做 isolate。** + 否決:這兩者作用在整個 stage 的合成層級,會讓 Scene Hierarchy + 面板、Property Panel 的選取與 viewport 的 pick 一併看不到/選不到 + 非成員物件,遠超出「只讓 viewport isolate」的實際需求。 +- **用 `UsdImagingGLEngine` 建構期的 `invisedPaths`,每次切換圖層就 + 重建 engine。** 否決:代價過高——會丟失 Hydra scene-index 狀態, + 以及漸進式渲染委派(Arnold/Cycles/Embree)已累積的取樣結果,並造成 + viewport 明顯閃爍,不適合互動式的頻繁圖層切換。 +- **用 anonymous `SdfLayer` 存放每個 render layer 的資料。** + 否決:anonymous layer 的 identifier 只在執行期有效,寫進 + `subLayerPaths` 後,在 `SaveStageAs`/重新開檔時無法被正確解析, + 資料會在存檔/重載之間遺失。 +- **用 `UsdVariantSet` 表示 render layer 之間互斥的選擇。** + 否決:Maya 的 collection + override 模型本質上更接近可疊加、 + 可獨立排序的 sublayer/override 合成,而非彼此互斥、一次只能選一 + 個的 variant 切換,語意上不夠貼合(尤其考慮到未來允許同一物件 + 同時屬於多個圖層的情境)。 + +## Verification + +本 ADR 記錄的是**實作前**的設計決策——目前尚未進行任何程式碼變更, +因此本節暫不回報建置或手動驗證結果。待對應的實作計畫 +(`misty-watching-stardust.md` 中記錄的 Phase 0/Phase 1 MVP)實際 +執行完成後,應在本節或另外新增一則後續紀錄中,補上實際的建置結果與 +手動驗證步驟/結果(圖層建立與切換、viewport isolate 是否正確、 +屬性覆寫是否正確落在對應圖層且不外洩到其他圖層、存檔重新開檔的 +round-trip、跨圖層切換的 Undo/Redo 行為),寫法比照 +`0001-viewport-color-correction.md` 文末的「Supersession Note」 +——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次 +決策當下的紀錄。 diff --git a/src/core/RenderLayerManager.cpp b/src/core/RenderLayerManager.cpp new file mode 100644 index 0000000..149056b --- /dev/null +++ b/src/core/RenderLayerManager.cpp @@ -0,0 +1,456 @@ +#include "RenderLayerManager.h" +#include "LayerManager.h" +#include "../utils/Logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerManager { + +namespace { +// Reserved, undefined ("over"-only) admin prim path each render layer's own +// SdfLayer carries its membership relationship on. Never a `def`, so it never +// shows up in UsdStage::Traverse() / the Scene Hierarchy panel / GetChildren(). +const SdfPath kAdminPrimPath("/RenderLayerData"); +const TfToken kMembersIncludesName("collection:members:includes"); + +// SdfLayer custom-layer-data keys. +const std::string kMarkerKey = "usdLayerManager:renderLayer"; +const std::string kNameKey = "usdLayerManager:renderLayerName"; +const std::string kAutoHiddenKey = "usdLayerManager:autoHiddenPaths"; +// Root-layer-only key: persists which render layer was active across save/reload. +const std::string kActiveLayerKey = "usdLayerManager:activeRenderLayer"; + +std::string SanitizeFileStem(const std::string& raw) { + std::string result; + result.reserve(raw.size()); + for (char c : raw) { + if (std::isalnum(static_cast(c)) || c == '_' || c == '-') + result += c; + else + result += '_'; + } + return result; +} + +SdfPath MembersIncludesPath() { + return kAdminPrimPath.AppendProperty(kMembersIncludesName); +} +} // namespace + +RenderLayerManager::RenderLayerManager() {} +RenderLayerManager::~RenderLayerManager() {} + +void RenderLayerManager::SetStage(UsdStageRefPtr stage) { + m_stage = stage; + Refresh(); +} + +void RenderLayerManager::Refresh() { + BuildList(); +} + +void RenderLayerManager::BuildList() { + m_layers.clear(); + m_layerRefs.clear(); + + RenderLayerInfo def; + def.name = "Default"; + def.isDefault = true; + m_layers.push_back(def); + + if (!m_stage) return; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return; + + bool anyActive = false; + for (const auto& subPath : rootLayer->GetSubLayerPaths()) { + SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath); + if (!sub) continue; + + VtDictionary cld = sub->GetCustomLayerData(); + auto markerIt = cld.find(kMarkerKey); + if (markerIt == cld.end() || !markerIt->second.IsHolding() || + !markerIt->second.Get()) + continue; // not a render layer sublayer — leave for LayerManager/StageEditorPanel + + m_layerRefs.push_back(sub); + + RenderLayerInfo info; + info.layer = SdfLayerHandle(sub); + info.layerIdentifier = sub->GetIdentifier(); + auto nameIt = cld.find(kNameKey); + info.name = (nameIt != cld.end() && nameIt->second.IsHolding()) + ? nameIt->second.Get() + : LayerManager::ExtractDisplayName(info.layerIdentifier); + info.isActive = !m_stage->IsLayerMuted(info.layerIdentifier); + if (info.isActive) anyActive = true; + + m_layers.push_back(info); + } + + m_layers[0].isActive = !anyActive; +} + +/*static*/ bool RenderLayerManager::IsRenderLayerSublayer(const SdfLayerHandle& layer) { + if (!layer) return false; + VtDictionary cld = layer->GetCustomLayerData(); + auto it = cld.find(kMarkerKey); + return it != cld.end() && it->second.IsHolding() && it->second.Get(); +} + +void RenderLayerManager::RefreshLayerManager() { + if (m_layerManager) m_layerManager->Refresh(); +} + +SdfLayerHandle RenderLayerManager::FindRenderLayer(const std::string& layerId) const { + for (const auto& info : m_layers) + if (!info.isDefault && info.layerIdentifier == layerId) + return info.layer; + return SdfLayerHandle(); +} + +// --------------------------------------------------------------------------- +// CRUD +// --------------------------------------------------------------------------- + +std::string RenderLayerManager::CreateRenderLayer(const std::string& name) { + if (!m_stage) return {}; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return {}; + + namespace fs = std::filesystem; + fs::path baseDir; + std::string realPath = rootLayer->GetRealPath(); + baseDir = !realPath.empty() + ? fs::path(realPath).parent_path() / ".renderlayers" + : fs::current_path() / ".renderlayers"; + std::error_code ec; + fs::create_directories(baseDir, ec); + + std::string stem = SanitizeFileStem(name.empty() ? "RenderLayer" : name); + if (stem.empty()) stem = "RenderLayer"; + std::string finalStem = stem; + fs::path filePath = baseDir / (finalStem + ".usda"); + int suffix = 1; + while (fs::exists(filePath)) { + finalStem = stem + "_" + std::to_string(suffix++); + filePath = baseDir / (finalStem + ".usda"); + } + + SdfLayerRefPtr newLayer = SdfLayer::CreateNew(filePath.string()); + if (!newLayer) { + LOG_ERROR("RenderLayerManager: failed to create layer file: " + filePath.string()); + return {}; + } + + VtDictionary cld; + cld[kMarkerKey] = VtValue(true); + cld[kNameKey] = VtValue(name.empty() ? finalStem : name); + newLayer->SetCustomLayerData(cld); + + SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(newLayer, kAdminPrimPath); + if (adminPrim) + SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(), + /*custom=*/false, SdfVariabilityUniform); + + newLayer->Save(); + + rootLayer->InsertSubLayerPath(filePath.string(), 0); + m_stage->MuteLayer(filePath.string()); // starts inactive until explicitly activated + + Refresh(); + RefreshLayerManager(); + return newLayer->GetIdentifier(); +} + +bool RenderLayerManager::DeleteRenderLayer(const std::string& layerId) { + if (!m_stage) return false; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return false; + if (!FindRenderLayer(layerId)) return false; + + if (GetActiveRenderLayerId() == layerId) + ActivateDefaultLayer(); + + SdfSubLayerProxy subPaths = rootLayer->GetSubLayerPaths(); + for (size_t i = 0; i < subPaths.size(); ++i) { + SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPaths[i]); + if (sub && sub->GetIdentifier() == layerId) { + subPaths.erase(subPaths.begin() + static_cast(i)); + Refresh(); + RefreshLayerManager(); + return true; + } + } + return false; +} + +bool RenderLayerManager::RestoreDeletedRenderLayer(const std::string& layerIdentifier) { + if (!m_stage) return false; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return false; + rootLayer->InsertSubLayerPath(layerIdentifier, 0); + Refresh(); + RefreshLayerManager(); + return true; +} + +bool RenderLayerManager::RenameRenderLayer(const std::string& layerId, const std::string& newName) { + SdfLayerHandle layer = FindRenderLayer(layerId); + if (!layer || newName.empty()) return false; + VtDictionary cld = layer->GetCustomLayerData(); + cld[kNameKey] = VtValue(newName); + layer->SetCustomLayerData(cld); + Refresh(); + RefreshLayerManager(); + return true; +} + +// --------------------------------------------------------------------------- +// Switching +// --------------------------------------------------------------------------- + +bool RenderLayerManager::ActivateRenderLayer(const std::string& layerId) { + if (!m_stage) return false; + SdfLayerHandle target = FindRenderLayer(layerId); + if (!target) return false; + + // First entry into render-layer mode this "session" (Default -> a render + // layer): remember the edit target so ActivateDefaultLayer() can restore + // it later, no matter how many render-layer-to-render-layer switches + // happen in between. + if (IsDefaultActive()) { + SdfLayerHandle currentEt = m_stage->GetEditTarget().GetLayer(); + m_savedEditTargetIdBeforeMode = currentEt ? currentEt->GetIdentifier() : std::string(); + } + + for (const auto& info : m_layers) { + if (info.isDefault || info.layerIdentifier == layerId) continue; + if (!m_stage->IsLayerMuted(info.layerIdentifier)) + m_stage->MuteLayer(info.layerIdentifier); + } + if (m_stage->IsLayerMuted(layerId)) + m_stage->UnmuteLayer(layerId); + + if (m_layerManager) m_layerManager->SetEditTarget(layerId); + else m_stage->SetEditTarget(UsdEditTarget(target)); + + RecomputeIsolation(target, layerId); + PersistActiveLayerId(layerId); + Refresh(); + RefreshLayerManager(); + return true; +} + +void RenderLayerManager::ActivateDefaultLayer() { + if (!m_stage) return; + for (const auto& info : m_layers) { + if (info.isDefault) continue; + if (!m_stage->IsLayerMuted(info.layerIdentifier)) + m_stage->MuteLayer(info.layerIdentifier); + } + + if (!m_savedEditTargetIdBeforeMode.empty()) { + if (m_layerManager) { + m_layerManager->SetEditTarget(m_savedEditTargetIdBeforeMode); + } else if (SdfLayerHandle saved = SdfLayer::Find(m_savedEditTargetIdBeforeMode)) { + m_stage->SetEditTarget(UsdEditTarget(saved)); + } + } + m_savedEditTargetIdBeforeMode.clear(); + + PersistActiveLayerId(""); + Refresh(); + RefreshLayerManager(); +} + +std::string RenderLayerManager::GetActiveRenderLayerId() const { + for (const auto& info : m_layers) + if (!info.isDefault && info.isActive) + return info.layerIdentifier; + return {}; +} + +void RenderLayerManager::PersistActiveLayerId(const std::string& layerId) { + if (!m_stage) return; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return; + VtDictionary cld = rootLayer->GetCustomLayerData(); + if (layerId.empty()) cld.erase(kActiveLayerKey); + else cld[kActiveLayerKey] = VtValue(layerId); + rootLayer->SetCustomLayerData(cld); +} + +void RenderLayerManager::RestorePersistedActiveLayer() { + if (!m_stage) return; + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + if (!rootLayer) return; + VtDictionary cld = rootLayer->GetCustomLayerData(); + auto it = cld.find(kActiveLayerKey); + if (it == cld.end() || !it->second.IsHolding()) return; + std::string layerId = it->second.Get(); + if (FindRenderLayer(layerId)) + ActivateRenderLayer(layerId); +} + +// --------------------------------------------------------------------------- +// Membership (Sdf-level — works regardless of mute state) +// --------------------------------------------------------------------------- + +std::vector RenderLayerManager::GetMembers(const std::string& layerId) const { + std::vector result; + SdfLayerHandle layer = FindRenderLayer(layerId); + if (!layer) return result; + SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath()); + if (!rel) return result; + for (const SdfPath& p : rel->GetTargetPathList().GetAddedOrExplicitItems()) + result.push_back(p); + return result; +} + +bool RenderLayerManager::AddMember(const std::string& layerId, const SdfPath& path) { + SdfLayerHandle layer = FindRenderLayer(layerId); + if (!layer) return false; + + SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath()); + if (!rel) { + SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(layer, kAdminPrimPath); + if (!adminPrim) return false; + rel = SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(), + /*custom=*/false, SdfVariabilityUniform); + } + if (!rel) return false; + + // Add() doesn't de-duplicate against existing explicit items — guard + // here so repeated adds of the same path (e.g. re-running a command, or + // a redundant drag) don't pile up duplicate targets. + auto existing = rel->GetTargetPathList().GetAddedOrExplicitItems(); + if (std::find(existing.begin(), existing.end(), path) == existing.end()) + rel->GetTargetPathList().Add(path); + + if (GetActiveRenderLayerId() == layerId) + RecomputeIsolation(layer, layerId); + RefreshLayerManager(); // keeps StageEditorPanel's dirty-flag asterisk current + return true; +} + +bool RenderLayerManager::RemoveMember(const std::string& layerId, const SdfPath& path) { + SdfLayerHandle layer = FindRenderLayer(layerId); + if (!layer) return false; + SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath()); + if (!rel) return false; + + rel->GetTargetPathList().Remove(path); + + if (GetActiveRenderLayerId() == layerId) + RecomputeIsolation(layer, layerId); + RefreshLayerManager(); + return true; +} + +// --------------------------------------------------------------------------- +// Viewport isolation +// --------------------------------------------------------------------------- + +void RenderLayerManager::ClearAutoHiddenPaths(const SdfLayerHandle& layer) { + if (!layer) return; + VtDictionary cld = layer->GetCustomLayerData(); + auto it = cld.find(kAutoHiddenKey); + if (it == cld.end()) return; + + if (it->second.IsHolding>()) { + for (const std::string& p : it->second.Get>()) { + SdfPath primPath(p); + SdfPrimSpecHandle owner = layer->GetPrimAtPath(primPath); + SdfAttributeSpecHandle attrSpec = + layer->GetAttributeAtPath(primPath.AppendProperty(TfToken("visibility"))); + if (owner && attrSpec) + owner->RemoveProperty(attrSpec); + } + } + cld.erase(it); + layer->SetCustomLayerData(cld); +} + +void RenderLayerManager::RecomputeIsolation(const SdfLayerHandle& layer, const std::string& layerId) { + if (!m_stage || !layer) return; + + ClearAutoHiddenPaths(layer); + + std::vector memberList = GetMembers(layerId); + std::set members(memberList.begin(), memberList.end()); + + // Every strict prefix of every member path: a subtree rooted at one of + // these paths might still contain a member somewhere below it, so it + // must be recursed into rather than hidden outright. + std::set ancestors; + for (const SdfPath& m : memberList) { + SdfPath p = m.GetParentPath(); + while (!p.IsEmpty()) { + if (!ancestors.insert(p).second) break; // already inserted by another member + if (p == SdfPath::AbsoluteRootPath()) break; + p = p.GetParentPath(); + } + } + + std::vector newHidden; + + std::function visit = [&](const UsdPrim& prim) { + for (const UsdPrim& child : prim.GetChildren()) { + SdfPath cp = child.GetPath(); + if (cp == kAdminPrimPath) continue; + + if (members.count(cp)) { + // Exact member: whole subtree stays default-visible (expandPrims + // semantics — everything under a member is implicitly included), + // nothing to author, no need to recurse. + continue; + } + if (ancestors.count(cp)) { + visit(child); // a member lies somewhere below — keep exploring + continue; + } + + UsdGeomImageable img(child); + if (img) { + UsdEditContext ec(m_stage, layer); + img.GetVisibilityAttr().Set(UsdGeomTokens->invisible); + newHidden.push_back(cp.GetString()); + // Minimal cut: stop here, children inherit the hidden state. + } else { + // Can't author visibility on a non-imageable prim (e.g. a pure + // Scope holding unrelated data) — look deeper for an imageable + // descendant to hide instead, rather than silently failing to + // isolate this whole branch. + visit(child); + } + } + }; + visit(m_stage->GetPseudoRoot()); + + if (!newHidden.empty()) { + VtDictionary cld = layer->GetCustomLayerData(); + cld[kAutoHiddenKey] = VtValue(VtArray(newHidden.begin(), newHidden.end())); + layer->SetCustomLayerData(cld); + } +} + +} // namespace UsdLayerManager diff --git a/src/core/RenderLayerManager.h b/src/core/RenderLayerManager.h new file mode 100644 index 0000000..8cb3cbb --- /dev/null +++ b/src/core/RenderLayerManager.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace UsdLayerManager { + +class LayerManager; + +/// One row for the Render Layer panel. The "Default" entry (isDefault=true) +/// has no backing layer — it represents the unmodified base stage, i.e. every +/// render-layer sublayer muted. +struct RenderLayerInfo { + std::string name; + pxr::SdfLayerHandle layer; ///< empty for Default + std::string layerIdentifier; ///< empty for Default; the stable key used everywhere below + bool isActive = false; + bool isDefault = false; +}; + +/// Maya-Render-Layers-style layers built on the stage's own sublayer stack. +/// +/// Each render layer is a dedicated, file-backed SdfLayer inserted as a +/// sublayer of the stage's root layer (tagged via SdfLayer custom layer data +/// so it's distinguishable from ordinary content sublayers). At most one is +/// ever unmuted ("active") at a time; activating one mutes the rest and +/// repoints the stage's ambient edit target at it, so every existing +/// attribute/transform/shader-connection/material-binding command in the app +/// already lands its edits in the right place with no changes of its own +/// (see docs/adr/0002-render-layer.md). +/// +/// Membership (which lights/objects belong to a layer) is stored as a +/// UsdCollectionAPI-compatible relationship (`collection:members:includes`) +/// on a reserved, undefined ("over"-only, never rendered) admin prim inside +/// that layer's own SdfLayer. All reads/writes of it go through raw Sdf-level +/// API rather than UsdCollectionAPI/UsdStage, because muted layers are fully +/// excluded from stage composition — GetPrimAtPath would fail for every +/// layer except the currently-active one, and the panel needs to edit +/// membership on layers that aren't active. +/// +/// Switching layers also isolates the viewport to that layer's members: a +/// single top-down traversal authors the minimal set of +/// `visibility = invisible` opinions (into the active layer's own SdfLayer) +/// needed to hide every subtree that contains no member, tracked via that +/// layer's custom layer data so it can be precisely cleared and recomputed +/// without disturbing deliberate visibility overrides. +class RenderLayerManager { +public: + RenderLayerManager(); + ~RenderLayerManager(); + + void SetStage(pxr::UsdStageRefPtr stage); + void SetLayerManager(LayerManager* mgr) { m_layerManager = mgr; } + void Refresh(); + + /// Default first, then user-created render layers in sublayer order. + std::vector GetRenderLayers() const { return m_layers; } + + /// Creates a new file-backed render layer next to the stage's root layer + /// (under a ".renderlayers" subdirectory), muted, with an empty member + /// list. Returns the new layer's identifier, or empty on failure. + std::string CreateRenderLayer(const std::string& name); + bool DeleteRenderLayer(const std::string& layerId); + /// Re-inserts a render layer previously removed by DeleteRenderLayer — its + /// backing file is left on disk untouched by DeleteRenderLayer, so this + /// just restores the sublayer reference (always at the top of the stack, + /// same as a newly-created layer). Used by RenderLayerDeleteCommand::Undo(). + bool RestoreDeletedRenderLayer(const std::string& layerIdentifier); + bool RenameRenderLayer(const std::string& layerId, const std::string& newName); + + /// Mutes every other render-layer sublayer, unmutes layerId, repoints the + /// stage's ambient edit target at it, and recomputes viewport isolation. + /// The first time this is called while Default is active, it remembers + /// the current edit target so ActivateDefaultLayer() can restore it later + /// — callers (e.g. an undoable switch command) don't need to manage that + /// themselves; it's tracked here so it stays correct across Undo/Redo. + bool ActivateRenderLayer(const std::string& layerId); + /// Mutes every render-layer sublayer (back to the unmodified base stage) + /// and restores whatever the edit target was before render-layer mode + /// was first entered (see ActivateRenderLayer's note). + void ActivateDefaultLayer(); + std::string GetActiveRenderLayerId() const; + bool IsDefaultActive() const { return GetActiveRenderLayerId().empty(); } + + /// Sdf-level: the raw, unexpanded `includes` target list. Works + /// regardless of whether layerId is currently muted. + std::vector GetMembers(const std::string& layerId) const; + bool AddMember(const std::string& layerId, const pxr::SdfPath& path); + bool RemoveMember(const std::string& layerId, const pxr::SdfPath& path); + + /// Re-applies whichever render layer was active the last time this stage + /// was saved (persisted as root-layer custom layer data, since USD mute + /// state itself is a runtime flag and isn't serialized). Call once after + /// SetStage(). No-op if nothing was persisted, or the persisted layer no + /// longer exists. + void RestorePersistedActiveLayer(); + + /// True if this layer carries the "this sublayer is a render layer" + /// marker RenderLayerManager itself authors on creation. Exposed so + /// other panels (e.g. StageEditorPanel) that walk the raw sublayer list + /// can recognize and guard-rail render-layer sublayers without + /// duplicating the custom-layer-data key. + static bool IsRenderLayerSublayer(const pxr::SdfLayerHandle& layer); + +private: + void BuildList(); + // LayerManager keeps its own cached sublayer/mute-state snapshot, + // rebuilt only on its own SetStage()/Refresh()/mutator calls — it has no + // way to know when RenderLayerManager mutates the root layer's sublayer + // stack or a layer's mute state directly (bypassing LayerManager's own + // methods, which every RenderLayerManager mutator below does). Called + // after every such mutation so StageEditorPanel (backed by LayerManager) + // doesn't show a stale sublayer list. + void RefreshLayerManager(); + pxr::SdfLayerHandle FindRenderLayer(const std::string& layerId) const; + void RecomputeIsolation(const pxr::SdfLayerHandle& layer, const std::string& layerId); + void ClearAutoHiddenPaths(const pxr::SdfLayerHandle& layer); + void PersistActiveLayerId(const std::string& layerId); + + pxr::UsdStageRefPtr m_stage; + LayerManager* m_layerManager = nullptr; + std::vector m_layers; // [0] is always the Default entry + // Strong refs so muted render-layer SdfLayers stay alive between + // Refresh() calls (mirrors LayerManager::m_layerRefs). + std::vector m_layerRefs; + // Edit target identifier captured the moment render-layer mode is first + // entered (Default -> any render layer); restored and cleared by + // ActivateDefaultLayer(). Empty when not currently "in mode". + std::string m_savedEditTargetIdBeforeMode; +}; + +} // namespace UsdLayerManager diff --git a/src/core/commands/ConnectShaderAttrsCommand.cpp b/src/core/commands/ConnectShaderAttrsCommand.cpp index 689c3ba..dd71aa0 100644 --- a/src/core/commands/ConnectShaderAttrsCommand.cpp +++ b/src/core/commands/ConnectShaderAttrsCommand.cpp @@ -1,5 +1,6 @@ #include "ConnectShaderAttrsCommand.h" #include "../../utils/Logger.h" +#include #include #include #include @@ -12,7 +13,8 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage, const pxr::SdfValueTypeName& destType, const pxr::SdfPath& sourceNode, const std::string& sourceOutput, - const pxr::SdfValueTypeName& sourceType) + const pxr::SdfValueTypeName& sourceType, + pxr::SdfLayerHandle editLayer) : m_stage(stage) , m_destNode(destNode) , m_destInput(destInput) @@ -20,6 +22,7 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage, , m_sourceNode(sourceNode) , m_sourceOutput(sourceOutput) , m_sourceType(sourceType) + , m_editLayer(editLayer) , m_description("Connect " + sourceNode.GetName() + "." + sourceOutput + " -> " + destNode.GetName() + "." + destInput) { @@ -50,9 +53,16 @@ void ConnectShaderAttrsCommand::Execute() { return; } - pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType); - pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType); - pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput); + if (m_editLayer) { + pxr::UsdEditContext ec(m_stage, m_editLayer); + pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType); + pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType); + pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput); + } else { + pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType); + pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType); + pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput); + } } catch (const std::exception& e) { LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what()); } @@ -66,13 +76,22 @@ void ConnectShaderAttrsCommand::Undo() { pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput)); if (!destInput) return; - if (m_hadPriorConnection) { - pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode)); - pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput)); - if (priorSourceShader && priorSourceOutput) - pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput); + auto doUndo = [&]() { + if (m_hadPriorConnection) { + pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode)); + pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput)); + if (priorSourceShader && priorSourceOutput) + pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput); + } else { + pxr::UsdShadeConnectableAPI::DisconnectSource(destInput); + } + }; + + if (m_editLayer) { + pxr::UsdEditContext ec(m_stage, m_editLayer); + doUndo(); } else { - pxr::UsdShadeConnectableAPI::DisconnectSource(destInput); + doUndo(); } } catch (const std::exception& e) { LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what()); diff --git a/src/core/commands/ConnectShaderAttrsCommand.h b/src/core/commands/ConnectShaderAttrsCommand.h index ec3106e..1c3c4d0 100644 --- a/src/core/commands/ConnectShaderAttrsCommand.h +++ b/src/core/commands/ConnectShaderAttrsCommand.h @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace UsdLayerManager { @@ -14,13 +15,19 @@ namespace UsdLayerManager { /// Undo can restore it exactly rather than merely disconnecting. class ConnectShaderAttrsCommand : public ICommand { public: + /// editLayer: layer to author into (mirrors TransformCommand's pattern). + /// Captured at construction time so Undo() targets the same layer as + /// Execute() even if the stage's ambient edit target changes in between + /// (e.g. a render-layer switch) — pass null/empty to fall back to + /// whatever the ambient edit target is at call time (today's behavior). ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& destNode, const std::string& destInput, const pxr::SdfValueTypeName& destType, const pxr::SdfPath& sourceNode, const std::string& sourceOutput, - const pxr::SdfValueTypeName& sourceType); + const pxr::SdfValueTypeName& sourceType, + pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle()); void Execute() override; void Undo() override; @@ -34,6 +41,7 @@ private: pxr::SdfPath m_sourceNode; std::string m_sourceOutput; pxr::SdfValueTypeName m_sourceType; + pxr::SdfLayerHandle m_editLayer; std::string m_description; bool m_hadPriorConnection = false; diff --git a/src/core/commands/DisconnectShaderAttrCommand.cpp b/src/core/commands/DisconnectShaderAttrCommand.cpp index 26df65c..0ad1eb2 100644 --- a/src/core/commands/DisconnectShaderAttrCommand.cpp +++ b/src/core/commands/DisconnectShaderAttrCommand.cpp @@ -1,5 +1,6 @@ #include "DisconnectShaderAttrCommand.h" #include "../../utils/Logger.h" +#include #include #include #include @@ -8,10 +9,12 @@ namespace UsdLayerManager { DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& destNode, - const std::string& destInput) + const std::string& destInput, + pxr::SdfLayerHandle editLayer) : m_stage(stage) , m_destNode(destNode) , m_destInput(destInput) + , m_editLayer(editLayer) , m_description("Disconnect " + destNode.GetName() + "." + destInput) { if (!stage) return; @@ -38,7 +41,13 @@ void DisconnectShaderAttrCommand::Execute() { if (!destShader) return; pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput)); if (!input) return; - pxr::UsdShadeConnectableAPI::DisconnectSource(input); + + if (m_editLayer) { + pxr::UsdEditContext ec(m_stage, m_editLayer); + pxr::UsdShadeConnectableAPI::DisconnectSource(input); + } else { + pxr::UsdShadeConnectableAPI::DisconnectSource(input); + } } catch (const std::exception& e) { LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what()); } @@ -53,8 +62,14 @@ void DisconnectShaderAttrCommand::Undo() { pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput)); pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput)); - if (input && output) + if (!input || !output) return; + + if (m_editLayer) { + pxr::UsdEditContext ec(m_stage, m_editLayer); pxr::UsdShadeConnectableAPI::ConnectToSource(input, output); + } else { + pxr::UsdShadeConnectableAPI::ConnectToSource(input, output); + } } catch (const std::exception& e) { LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what()); } diff --git a/src/core/commands/DisconnectShaderAttrCommand.h b/src/core/commands/DisconnectShaderAttrCommand.h index 619aff6..f9d40f6 100644 --- a/src/core/commands/DisconnectShaderAttrCommand.h +++ b/src/core/commands/DisconnectShaderAttrCommand.h @@ -3,6 +3,7 @@ #include "../CommandHistory.h" #include #include +#include #include namespace UsdLayerManager { @@ -11,9 +12,13 @@ namespace UsdLayerManager { /// source at construction time so Undo can restore it. class DisconnectShaderAttrCommand : public ICommand { public: + /// editLayer: see ConnectShaderAttrsCommand's constructor doc — same + /// capture-at-construction-time pattern, null/empty falls back to the + /// ambient edit target at call time (today's behavior). DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& destNode, - const std::string& destInput); + const std::string& destInput, + pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle()); void Execute() override; void Undo() override; @@ -23,6 +28,7 @@ private: pxr::UsdStageRefPtr m_stage; pxr::SdfPath m_destNode; std::string m_destInput; + pxr::SdfLayerHandle m_editLayer; std::string m_description; bool m_hadConnection = false; diff --git a/src/core/commands/RenderLayerCommands.cpp b/src/core/commands/RenderLayerCommands.cpp new file mode 100644 index 0000000..401f4e5 --- /dev/null +++ b/src/core/commands/RenderLayerCommands.cpp @@ -0,0 +1,102 @@ +#include "RenderLayerCommands.h" + +namespace UsdLayerManager { + +// ─── RenderLayerCreateCommand ───────────────────────────────────────────── + +RenderLayerCreateCommand::RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name) + : m_mgr(mgr) + , m_name(std::move(name)) + , m_description("Create Render Layer " + m_name) +{ +} + +void RenderLayerCreateCommand::Execute() { + if (!m_mgr) return; + m_layerId = m_mgr->CreateRenderLayer(m_name); +} + +void RenderLayerCreateCommand::Undo() { + if (!m_mgr || m_layerId.empty()) return; + m_mgr->DeleteRenderLayer(m_layerId); +} + +// ─── RenderLayerDeleteCommand ───────────────────────────────────────────── + +RenderLayerDeleteCommand::RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId) + : m_mgr(mgr) + , m_layerId(std::move(layerId)) + , m_description("Delete Render Layer") +{ + if (!mgr) return; + for (const auto& info : mgr->GetRenderLayers()) { + if (!info.isDefault && info.layerIdentifier == m_layerId) { + m_description = "Delete Render Layer " + info.name; + break; + } + } +} + +void RenderLayerDeleteCommand::Execute() { + if (!m_mgr) return; + m_mgr->DeleteRenderLayer(m_layerId); +} + +void RenderLayerDeleteCommand::Undo() { + if (!m_mgr || m_layerId.empty()) return; + m_mgr->RestoreDeletedRenderLayer(m_layerId); +} + +// ─── RenderLayerRenameCommand ───────────────────────────────────────────── + +RenderLayerRenameCommand::RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId, + std::string newName) + : m_mgr(mgr) + , m_layerId(std::move(layerId)) + , m_newName(std::move(newName)) + , m_description("Rename Render Layer") +{ + if (!mgr) return; + for (const auto& info : mgr->GetRenderLayers()) { + if (!info.isDefault && info.layerIdentifier == m_layerId) { + m_oldName = info.name; + m_description = "Rename Render Layer " + info.name + " -> " + m_newName; + break; + } + } +} + +void RenderLayerRenameCommand::Execute() { + if (!m_mgr) return; + m_mgr->RenameRenderLayer(m_layerId, m_newName); +} + +void RenderLayerRenameCommand::Undo() { + if (!m_mgr || m_oldName.empty()) return; + m_mgr->RenameRenderLayer(m_layerId, m_oldName); +} + +// ─── RenderLayerActivateCommand ─────────────────────────────────────────── + +RenderLayerActivateCommand::RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId) + : m_mgr(mgr) + , m_targetLayerId(std::move(targetLayerId)) + , m_description(m_targetLayerId.empty() ? "Activate Render Layer: Default" + : "Activate Render Layer") +{ + if (mgr) m_priorLayerId = mgr->GetActiveRenderLayerId(); +} + +void RenderLayerActivateCommand::Execute() { + if (!m_mgr) return; + if (m_targetLayerId.empty()) m_mgr->ActivateDefaultLayer(); + else m_mgr->ActivateRenderLayer(m_targetLayerId); +} + +void RenderLayerActivateCommand::Undo() { + if (!m_mgr) return; + if (m_priorLayerId.empty()) m_mgr->ActivateDefaultLayer(); + else m_mgr->ActivateRenderLayer(m_priorLayerId); +} + +} // namespace UsdLayerManager diff --git a/src/core/commands/RenderLayerCommands.h b/src/core/commands/RenderLayerCommands.h new file mode 100644 index 0000000..3939705 --- /dev/null +++ b/src/core/commands/RenderLayerCommands.h @@ -0,0 +1,76 @@ +#pragma once + +#include "../CommandHistory.h" +#include "../RenderLayerManager.h" +#include + +namespace UsdLayerManager { + +/// Undo removes the created layer via RenderLayerManager::DeleteRenderLayer. +class RenderLayerCreateCommand : public ICommand { +public: + RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + + /// Valid after Execute() — the new layer's identifier, so the panel can + /// select it immediately. + const std::string& GetCreatedLayerId() const { return m_layerId; } + +private: + RenderLayerManager* m_mgr; + std::string m_name; + std::string m_layerId; + std::string m_description; +}; + +/// The backing file is left on disk by RenderLayerManager::DeleteRenderLayer +/// (only the sublayer reference is removed), so Undo just re-inserts it. +class RenderLayerDeleteCommand : public ICommand { +public: + RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + RenderLayerManager* m_mgr; + std::string m_layerId; + std::string m_description; +}; + +class RenderLayerRenameCommand : public ICommand { +public: + RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId, std::string newName); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + RenderLayerManager* m_mgr; + std::string m_layerId; + std::string m_newName; + std::string m_oldName; + std::string m_description; +}; + +/// Switches the active render layer (or back to Default). RenderLayerManager +/// itself tracks the edit target to restore on returning to Default, so this +/// command only needs to remember which layer was active before the switch. +class RenderLayerActivateCommand : public ICommand { +public: + /// targetLayerId empty = activate Default. + RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + RenderLayerManager* m_mgr; + std::string m_targetLayerId; // "" = Default + std::string m_priorLayerId; // captured at construction; "" = Default + std::string m_description; +}; + +} // namespace UsdLayerManager diff --git a/src/core/commands/RenderLayerMembershipCommand.cpp b/src/core/commands/RenderLayerMembershipCommand.cpp new file mode 100644 index 0000000..16a8deb --- /dev/null +++ b/src/core/commands/RenderLayerMembershipCommand.cpp @@ -0,0 +1,57 @@ +#include "RenderLayerMembershipCommand.h" + +namespace UsdLayerManager { + +// ─── RenderLayerAddMembersCommand ───────────────────────────────────────── + +RenderLayerAddMembersCommand::RenderLayerAddMembersCommand(RenderLayerManager* mgr, + std::string layerId, + std::vector paths) + : m_mgr(mgr) + , m_layerId(std::move(layerId)) + , m_paths(std::move(paths)) + , m_description(m_paths.size() == 1 + ? ("Add " + m_paths.front().GetString() + " to Render Layer") + : ("Add " + std::to_string(m_paths.size()) + " Members to Render Layer")) +{ +} + +void RenderLayerAddMembersCommand::Execute() { + if (!m_mgr) return; + for (const auto& p : m_paths) + m_mgr->AddMember(m_layerId, p); +} + +void RenderLayerAddMembersCommand::Undo() { + if (!m_mgr) return; + for (const auto& p : m_paths) + m_mgr->RemoveMember(m_layerId, p); +} + +// ─── RenderLayerRemoveMembersCommand ────────────────────────────────────── + +RenderLayerRemoveMembersCommand::RenderLayerRemoveMembersCommand(RenderLayerManager* mgr, + std::string layerId, + std::vector paths) + : m_mgr(mgr) + , m_layerId(std::move(layerId)) + , m_paths(std::move(paths)) + , m_description(m_paths.size() == 1 + ? ("Remove " + m_paths.front().GetString() + " from Render Layer") + : ("Remove " + std::to_string(m_paths.size()) + " Members from Render Layer")) +{ +} + +void RenderLayerRemoveMembersCommand::Execute() { + if (!m_mgr) return; + for (const auto& p : m_paths) + m_mgr->RemoveMember(m_layerId, p); +} + +void RenderLayerRemoveMembersCommand::Undo() { + if (!m_mgr) return; + for (const auto& p : m_paths) + m_mgr->AddMember(m_layerId, p); +} + +} // namespace UsdLayerManager diff --git a/src/core/commands/RenderLayerMembershipCommand.h b/src/core/commands/RenderLayerMembershipCommand.h new file mode 100644 index 0000000..a87ec65 --- /dev/null +++ b/src/core/commands/RenderLayerMembershipCommand.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../CommandHistory.h" +#include "../RenderLayerManager.h" +#include +#include +#include + +namespace UsdLayerManager { + +/// Adds one or more prim paths to a render layer's membership (Sdf-level, so +/// this works whether or not layerId is currently active). Re-runs viewport +/// isolation if layerId happens to be the active layer. +class RenderLayerAddMembersCommand : public ICommand { +public: + RenderLayerAddMembersCommand(RenderLayerManager* mgr, std::string layerId, + std::vector paths); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + RenderLayerManager* m_mgr; + std::string m_layerId; + std::vector m_paths; + std::string m_description; +}; + +class RenderLayerRemoveMembersCommand : public ICommand { +public: + RenderLayerRemoveMembersCommand(RenderLayerManager* mgr, std::string layerId, + std::vector paths); + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + RenderLayerManager* m_mgr; + std::string m_layerId; + std::vector m_paths; + std::string m_description; +}; + +} // namespace UsdLayerManager diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 6a9bb7f..c59d37c 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -41,6 +41,7 @@ Application::Application() : m_showDemoWindow(false) , m_showStageInfo(true) , m_showStageEditor(true) + , m_showRenderLayerPanel(true) , m_showSceneHierarchy(true) , m_showViewport(true) , m_showPropertyPanel(true) @@ -67,15 +68,23 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig // Create managers m_stageManager = std::make_unique(); m_layerManager = std::make_unique(); + m_renderLayerManager = std::make_unique(); + m_renderLayerManager->SetLayerManager(m_layerManager.get()); m_propertyManager = std::make_unique(); m_propertyManager->SetCommandHistory(&m_commandHistory); m_stageEditorPanel = std::make_unique(); m_stageEditorPanel->SetLayerManager(m_layerManager.get()); m_stageEditorPanel->SetCommandHistory(&m_commandHistory); + m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get()); m_sceneHierarchyPanel = std::make_unique(); m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get()); m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory); m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get()); + m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get()); + m_renderLayerPanel = std::make_unique(); + m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get()); + m_renderLayerPanel->SetCommandHistory(&m_commandHistory); + m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get()); m_viewportPanel = std::make_unique(); m_viewportPanel->SetCommandHistory(&m_commandHistory); m_propertyPanel = std::make_unique(); @@ -116,6 +125,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig m_timelinePanel->SetIconManager(m_iconManager.get()); m_stageEditorPanel->SetIconManager(m_iconManager.get()); m_materialEditorPanel->SetIconManager(m_iconManager.get()); + m_renderLayerPanel->SetIconManager(m_iconManager.get()); m_sceneHierarchyPanel->SetOnPrimSelected( [this](const std::string& path) { @@ -235,10 +245,12 @@ void Application::Shutdown() { m_sceneHierarchyPanel.reset(); m_propertyPanel.reset(); m_stageEditorPanel.reset(); + m_renderLayerPanel.reset(); // Owns the shader-ball preview's Hydra engine + GL draw target — must be // destroyed while the GL context still exists, like m_viewportPanel. m_materialEditorPanel.reset(); m_propertyManager.reset(); + m_renderLayerManager.reset(); m_layerManager.reset(); if (m_iconManager) { @@ -263,8 +275,12 @@ void Application::RefreshManagers() { if (m_stageManager->HasStage()) { auto stage = m_stageManager->GetStage(); m_layerManager->SetStage(stage); + // After LayerManager (ActivateRenderLayer routes edit-target changes + // through it) but before RestorePersistedActiveLayer needs it. + m_renderLayerManager->SetStage(stage); m_propertyManager->SetStage(stage); m_sceneHierarchyPanel->SetStage(stage); + m_renderLayerPanel->SetStage(stage); m_viewportPanel->SetStage(stage); m_viewportPanel->FrameScene(); m_propertyPanel->SetStage(stage); @@ -272,10 +288,16 @@ void Application::RefreshManagers() { m_curveEditorPanel->SetStage(stage); m_materialManager->SetStage(stage); m_materialEditorPanel->SetStage(stage); + // Re-applies whichever render layer was active last time this stage + // was saved (mute state itself isn't serialized — see + // RenderLayerManager::RestorePersistedActiveLayer). + m_renderLayerManager->RestorePersistedActiveLayer(); } else { m_layerManager->SetStage(nullptr); + m_renderLayerManager->SetStage(nullptr); m_propertyManager->SetStage(nullptr); m_sceneHierarchyPanel->SetStage(nullptr); + m_renderLayerPanel->SetStage(nullptr); m_viewportPanel->SetStage(nullptr); m_propertyPanel->SetStage(nullptr); m_timelinePanel->SetStage(nullptr); @@ -327,6 +349,12 @@ void Application::RenderUI() { ImGui::End(); } + if (m_showRenderLayerPanel) { + ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse); + m_renderLayerPanel->Render(); + ImGui::End(); + } + if (m_showViewport) { m_viewportPanel->Render(&m_showViewport); } @@ -777,6 +805,7 @@ void Application::RenderMenuBar() { if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor); + ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel); ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy); ImGui::MenuItem("Viewport", nullptr, &m_showViewport); ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel); @@ -872,7 +901,9 @@ void Application::SaveUsdFile() { if (!m_stageManager->SaveStage()) { LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); + return; } + SaveDirtyRenderLayers(); } void Application::SaveUsdFileAs() { @@ -891,6 +922,11 @@ void Application::SaveUsdFileAs() { if (!m_stageManager->SaveStageAs(filePath)) { LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); } else { + // m_renderLayerManager's layer refs are identifier-keyed SdfLayer + // objects, independent of which UsdStage currently references + // them — safe to save before the stage swap RefreshManagers() + // below performs. + SaveDirtyRenderLayers(); // SaveStageAs reopens m_stageManager's stage from the new file path. // RefreshManagers syncs m_layerManager (and others) to that new stage; // without this, subsequent sublayer edits go to the old (now stale) stage @@ -900,6 +936,15 @@ void Application::SaveUsdFileAs() { } } +void Application::SaveDirtyRenderLayers() { + if (!m_renderLayerManager) return; + for (const auto& info : m_renderLayerManager->GetRenderLayers()) { + if (info.isDefault || !info.layer) continue; + if (info.layer->IsDirty()) + info.layer->Save(); + } +} + void Application::CloseUsdFile() { m_stageManager->CloseStage(); // Re-create a fresh default stage so the app is always in an editable state. diff --git a/src/ui/Application.h b/src/ui/Application.h index 25ed9f0..c58a0aa 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -4,6 +4,7 @@ #include "IconManager.h" #include "StageEditorPanel.h" #include "SceneHierarchyPanel.h" +#include "RenderLayerPanel.h" #include "ViewportPanel.h" #include "PropertyPanel.h" #include "TimelinePanel.h" @@ -11,6 +12,7 @@ #include "MaterialEditorPanel.h" #include "../core/UsdStageManager.h" #include "../core/LayerManager.h" +#include "../core/RenderLayerManager.h" #include "../core/PropertyManager.h" #include "../core/MaterialManager.h" #include "../core/CommandHistory.h" @@ -65,6 +67,12 @@ private: void AddReferenceToStage(); void CreatePrimOnStage(const std::string& typeName); + /// Explicitly saves every render-layer SdfLayer, since at most one is + /// ever in the stage's composition (unmuted) at a time and + /// UsdStage::Save() only walks layers currently in composition — the + /// rest would otherwise be silently dropped on save. + void SaveDirtyRenderLayers(); + // Playblast void CapturePlayblastFrame(); @@ -78,11 +86,13 @@ private: std::unique_ptr m_iconManager; std::unique_ptr m_stageManager; std::unique_ptr m_layerManager; + std::unique_ptr m_renderLayerManager; std::unique_ptr m_propertyManager; std::unique_ptr m_materialManager; CommandHistory m_commandHistory; std::unique_ptr m_stageEditorPanel; std::unique_ptr m_sceneHierarchyPanel; + std::unique_ptr m_renderLayerPanel; std::unique_ptr m_viewportPanel; std::unique_ptr m_propertyPanel; std::unique_ptr m_timelinePanel; @@ -91,6 +101,7 @@ private: bool m_showDemoWindow; bool m_showStageInfo; bool m_showStageEditor; + bool m_showRenderLayerPanel; bool m_showSceneHierarchy; bool m_showViewport; bool m_showPropertyPanel; diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index 63a4c5a..9059bc2 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -9,6 +9,7 @@ #include "../utils/Logger.h" #include "../utils/FileDialog.h" #include "../utils/OcioConfigParser.h" +#include #include #include #include @@ -1428,25 +1429,42 @@ void MaterialEditorPanel::BindMaterialToTarget(const pxr::SdfPath& materialPath, } pxr::UsdStageRefPtr stage = m_stage; - auto doBind = [stage, targetPath, materialPath]() { + // Captured at construction time so Undo() lands in the same layer as + // Execute() even if the ambient edit target changes in between (e.g. a + // render-layer switch) — mirrors ConnectShaderAttrsCommand's pattern. + pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer(); + auto doBind = [stage, targetPath, materialPath, editLayer]() { pxr::UsdPrim target = stage->GetPrimAtPath(targetPath); pxr::UsdShadeMaterial material(stage->GetPrimAtPath(materialPath)); - if (target && material) + if (!target || !material) return; + if (editLayer) { + pxr::UsdEditContext ec(stage, editLayer); pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material); + } else { + pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material); + } }; if (m_commandHistory) { m_commandHistory->Push(std::make_unique( "Bind Material", doBind, - [stage, targetPath, hadPriorRel, priorTargets]() { + [stage, targetPath, hadPriorRel, priorTargets, editLayer]() { pxr::UsdPrim target = stage->GetPrimAtPath(targetPath); if (!target) return; - pxr::UsdShadeMaterialBindingAPI bindAPI(target); - if (hadPriorRel && !priorTargets.empty()) - bindAPI.GetDirectBindingRel().SetTargets(priorTargets); - else - bindAPI.UnbindDirectBinding(); + auto doUnbind = [&]() { + pxr::UsdShadeMaterialBindingAPI bindAPI(target); + if (hadPriorRel && !priorTargets.empty()) + bindAPI.GetDirectBindingRel().SetTargets(priorTargets); + else + bindAPI.UnbindDirectBinding(); + }; + if (editLayer) { + pxr::UsdEditContext ec(stage, editLayer); + doUnbind(); + } else { + doUnbind(); + } } )); } else { @@ -2355,9 +2373,13 @@ pxr::SdfPath MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) { if (!m_stage || !m_commandHistory) return; + // Captured now so Undo() (which may run after the ambient edit target + // has since changed, e.g. a render-layer switch) still targets the same + // layer this connection was authored into. + pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer(); m_commandHistory->Push(std::make_unique( m_stage, destInput.nodePath, destInput.name, destInput.typeName, - sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName)); + sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName, editLayer)); SyncFromUsd(); } @@ -2369,7 +2391,8 @@ void MaterialEditorPanel::DeleteNode(const pxr::SdfPath& nodePath) { void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) { if (!m_stage || !m_commandHistory) return; - m_commandHistory->Push(std::make_unique(m_stage, destNode, destInput)); + pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer(); + m_commandHistory->Push(std::make_unique(m_stage, destNode, destInput, editLayer)); SyncFromUsd(); } diff --git a/src/ui/RenderLayerPanel.cpp b/src/ui/RenderLayerPanel.cpp new file mode 100644 index 0000000..4322046 --- /dev/null +++ b/src/ui/RenderLayerPanel.cpp @@ -0,0 +1,263 @@ +#include "RenderLayerPanel.h" +#include "../core/commands/RenderLayerCommands.h" +#include "../core/commands/RenderLayerMembershipCommand.h" +#include +#include +#include + +namespace UsdLayerManager { + +RenderLayerPanel::RenderLayerPanel() {} +RenderLayerPanel::~RenderLayerPanel() {} + +void RenderLayerPanel::SetStage(pxr::UsdStageRefPtr stage) { + m_stage = stage; + m_defaultSelected = true; + m_selectedLayerId.clear(); +} + +// --------------------------------------------------------------------------- +void RenderLayerPanel::Render() { + if (!m_renderLayerManager) { + ImGui::TextDisabled("No stage loaded"); + return; + } + + ImGui::Text("Render Layers"); + ImGui::Separator(); + + ImGui::SetNextItemWidth(180.0f); + ImGui::InputText("##newLayerName", m_newLayerNameBuf, sizeof(m_newLayerNameBuf)); + ImGui::SameLine(); + if (ImGui::Button("Create Layer")) { + std::string name = m_newLayerNameBuf[0] ? m_newLayerNameBuf : "RenderLayer"; + std::string newId; + if (m_commandHistory) { + auto cmd = std::make_unique(m_renderLayerManager, name); + RenderLayerCreateCommand* raw = cmd.get(); + m_commandHistory->Push(std::move(cmd)); + newId = raw->GetCreatedLayerId(); + } else { + newId = m_renderLayerManager->CreateRenderLayer(name); + } + if (!newId.empty()) { + m_defaultSelected = false; + m_selectedLayerId = newId; + } + } + + ImGui::Spacing(); + RenderLayerList(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextDisabled("Membership"); + ImGui::Separator(); + RenderMembershipPanel(); + + RenderRenameModal(); +} + +// --------------------------------------------------------------------------- +void RenderLayerPanel::RenderLayerList() { + auto layers = m_renderLayerManager->GetRenderLayers(); + + if (!ImGui::BeginTable("RenderLayerTable", 3, + ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY, + ImVec2(0, 150))) + return; + + ImGui::TableSetupColumn("##active", ImGuiTableColumnFlags_WidthFixed, 24.0f); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f); + + for (int i = 0; i < static_cast(layers.size()); ++i) { + const auto& li = layers[i]; + ImGui::TableNextRow(); + ImGui::PushID(i); + + // Col 0: activate radio button. + ImGui::TableSetColumnIndex(0); + { + bool isActive = li.isActive; + if (ImGui::RadioButton("##act", isActive) && !isActive) { + std::string target = li.isDefault ? std::string() : li.layerIdentifier; + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_renderLayerManager, target)); + else if (li.isDefault) + m_renderLayerManager->ActivateDefaultLayer(); + else + m_renderLayerManager->ActivateRenderLayer(target); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(isActive ? "Active" : "Click to activate"); + } + + // Col 1: name — selecting a row picks which layer's membership shows below. + ImGui::TableSetColumnIndex(1); + { + bool selected = li.isDefault + ? m_defaultSelected + : (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier); + ImVec4 nameColor = li.isActive ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) + : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, nameColor); + if (ImGui::Selectable(li.name.c_str(), selected, + ImGuiSelectableFlags_SpanAllColumns)) { + m_defaultSelected = li.isDefault; + m_selectedLayerId = li.isDefault ? std::string() : li.layerIdentifier; + } + ImGui::PopStyleColor(); + + if (!li.isDefault && ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Rename")) { + m_openRenameModal = true; + m_renameTargetId = li.layerIdentifier; + std::strncpy(m_renameBuf, li.name.c_str(), sizeof(m_renameBuf) - 1); + m_renameBuf[sizeof(m_renameBuf) - 1] = '\0'; + } + if (ImGui::MenuItem("Delete")) { + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_renderLayerManager, li.layerIdentifier)); + else + m_renderLayerManager->DeleteRenderLayer(li.layerIdentifier); + if (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier) { + m_defaultSelected = true; + m_selectedLayerId.clear(); + } + } + ImGui::EndPopup(); + } + } + + // Col 2: dirty indicator. + ImGui::TableSetColumnIndex(2); + if (!li.isDefault && li.layer && li.layer->IsDirty()) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*"); + else + ImGui::TextDisabled(" "); + + ImGui::PopID(); + } + + ImGui::EndTable(); +} + +// --------------------------------------------------------------------------- +void RenderLayerPanel::RenderMembershipPanel() { + if (m_defaultSelected) { + ImGui::TextDisabled("Default has no membership of its own - it always shows the full, unmodified stage."); + return; + } + if (m_selectedLayerId.empty()) { + ImGui::TextDisabled("Select a render layer above to edit its membership."); + return; + } + + std::string displayName = m_selectedLayerId; + for (const auto& li : m_renderLayerManager->GetRenderLayers()) + if (!li.isDefault && li.layerIdentifier == m_selectedLayerId) { displayName = li.name; break; } + ImGui::Text("Members of \"%s\"", displayName.c_str()); + + auto members = m_renderLayerManager->GetMembers(m_selectedLayerId); + + auto isValid = [&](const pxr::SdfPath& p) { + return m_stage && m_stage->GetPrimAtPath(p).IsValid(); + }; + + bool anyMissing = false; + for (const auto& p : members) + if (!isValid(p)) { anyMissing = true; break; } + + if (m_sceneHierarchyPanel && ImGui::Button("Add Selected")) { + auto paths = m_sceneHierarchyPanel->GetSelectedPaths(); + if (!paths.empty()) { + std::vector sdfPaths; + sdfPaths.reserve(paths.size()); + for (const auto& p : paths) sdfPaths.emplace_back(p); + if (m_commandHistory) + m_commandHistory->Push(std::make_unique( + m_renderLayerManager, m_selectedLayerId, sdfPaths)); + else + for (const auto& sp : sdfPaths) m_renderLayerManager->AddMember(m_selectedLayerId, sp); + } + } + if (anyMissing) { + ImGui::SameLine(); + if (ImGui::Button("Remove Missing")) { + std::vector toRemove; + for (const auto& p : members) + if (!isValid(p)) toRemove.push_back(p); + if (m_commandHistory) + m_commandHistory->Push(std::make_unique( + m_renderLayerManager, m_selectedLayerId, toRemove)); + else + for (const auto& p : toRemove) m_renderLayerManager->RemoveMember(m_selectedLayerId, p); + } + } + + if (members.empty()) { + ImGui::TextDisabled("No members. Select lights/objects in Scene Hierarchy and click \"Add Selected\"."); + return; + } + + if (ImGui::BeginChild("RenderLayerMembersList", ImVec2(0, 0), true)) { + for (const auto& path : members) { + ImGui::PushID(path.GetString().c_str()); + bool missing = !isValid(path); + + if (missing) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.45f, 1.0f)); + ImGui::TextUnformatted(path.GetText()); + if (missing) { + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextDisabled("(missing)"); + } + + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - 24.0f); + if (ImGui::SmallButton("x")) { + if (m_commandHistory) + m_commandHistory->Push(std::make_unique( + m_renderLayerManager, m_selectedLayerId, std::vector{path})); + else + m_renderLayerManager->RemoveMember(m_selectedLayerId, path); + } + ImGui::PopID(); + } + } + ImGui::EndChild(); +} + +// --------------------------------------------------------------------------- +void RenderLayerPanel::RenderRenameModal() { + if (m_openRenameModal) { + ImGui::OpenPopup("Rename Render Layer"); + m_openRenameModal = false; + } + if (ImGui::BeginPopupModal("Rename Render Layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::SetNextItemWidth(240.0f); + bool enterPressed = ImGui::InputText("Name", m_renameBuf, sizeof(m_renameBuf), + ImGuiInputTextFlags_EnterReturnsTrue); + bool okClicked = ImGui::Button("OK"); + ImGui::SameLine(); + bool cancelClicked = ImGui::Button("Cancel"); + + if ((enterPressed || okClicked) && m_renameBuf[0]) { + if (m_commandHistory) + m_commandHistory->Push(std::make_unique( + m_renderLayerManager, m_renameTargetId, std::string(m_renameBuf))); + else + m_renderLayerManager->RenameRenderLayer(m_renameTargetId, m_renameBuf); + ImGui::CloseCurrentPopup(); + } else if (cancelClicked) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +} // namespace UsdLayerManager diff --git a/src/ui/RenderLayerPanel.h b/src/ui/RenderLayerPanel.h new file mode 100644 index 0000000..53ffb66 --- /dev/null +++ b/src/ui/RenderLayerPanel.h @@ -0,0 +1,52 @@ +#pragma once + +#include "../core/RenderLayerManager.h" +#include "../core/CommandHistory.h" +#include "SceneHierarchyPanel.h" +#include "IconManager.h" +#include +#include + +namespace UsdLayerManager { + +/// Maya-Render-Layers-style panel: layer list with one-click activation, +/// create/rename/delete, and a membership editor for whichever layer is +/// selected (independent of which one is active — you can inspect/edit a +/// layer's members without switching to it). +class RenderLayerPanel { +public: + RenderLayerPanel(); + ~RenderLayerPanel(); + + void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; } + void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } + /// Optional — powers the "Add Selected" membership button. + void SetSceneHierarchyPanel(SceneHierarchyPanel* panel) { m_sceneHierarchyPanel = panel; } + void SetIconManager(IconManager* icons) { m_iconManager = icons; } + void SetStage(pxr::UsdStageRefPtr stage); + void Render(); + +private: + void RenderLayerList(); + void RenderMembershipPanel(); + void RenderRenameModal(); + + RenderLayerManager* m_renderLayerManager = nullptr; + CommandHistory* m_commandHistory = nullptr; + SceneHierarchyPanel* m_sceneHierarchyPanel = nullptr; + IconManager* m_iconManager = nullptr; + pxr::UsdStageRefPtr m_stage; + + /// Which layer's membership the lower panel shows — independent of which + /// layer is currently active. + bool m_defaultSelected = true; + std::string m_selectedLayerId; + + char m_newLayerNameBuf[128] = "RenderLayer"; + + bool m_openRenameModal = false; + std::string m_renameTargetId; + char m_renameBuf[128] = {}; +}; + +} // namespace UsdLayerManager diff --git a/src/ui/SceneHierarchyPanel.h b/src/ui/SceneHierarchyPanel.h index 7b18002..b652987 100644 --- a/src/ui/SceneHierarchyPanel.h +++ b/src/ui/SceneHierarchyPanel.h @@ -3,6 +3,7 @@ #include "../core/PropertyManager.h" #include "../core/CommandHistory.h" #include "../core/LayerManager.h" +#include "../core/RenderLayerManager.h" #include "IconManager.h" #include #include @@ -26,13 +27,24 @@ public: void SetPropertyManager(PropertyManager* manager); void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetLayerManager(LayerManager* lm) { m_layerManager = lm; } + /// Optional. When set and a non-Default render layer is active, the + /// edit-target layer switcher below is disabled — repointing the ambient + /// edit target away from the active render layer's SdfLayer would break + /// the invariant that edits made while it's active land in that layer. + void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; } void SetStage(UsdStageRefPtr stage); void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; } void Render(); - + std::string GetSelectedPrimPath() const { return m_primarySelectedPath; } UsdPrim GetSelectedPrim() const; + /// Full multi-selection (rect pick or Ctrl/Shift click), in no + /// particular order. Empty when nothing is selected. + std::vector GetSelectedPaths() const { + return std::vector(m_selectedPaths.begin(), m_selectedPaths.end()); + } + /// Set single selection from hierarchy click (fires callback). /// Also clears any rect multi-selection. void SetSelectedPathFromClick(const std::string& path); @@ -79,10 +91,13 @@ private: void RenderRemovePrimModal(); void RenderArcModal(); void ProcessPendingReplaceRef(); + void RenderSublayerSection(); + void RenderSublayerContextMenu(int i, const std::vector& sublayers); PropertyManager* m_propertyManager; CommandHistory* m_commandHistory = nullptr; LayerManager* m_layerManager = nullptr; + RenderLayerManager* m_renderLayerManager = nullptr; IconManager* m_iconManager = nullptr; UsdStageRefPtr m_stage; @@ -94,6 +109,9 @@ private: bool m_scrollToSelected = false; + /// Selected row in the Sublayers section (context menu / drag anchor). + int m_sublayerSelectedIdx = -1; + /// Stage-local layer identifiers rebuilt once per Render() for override detection. /// Contains only the stage's own layers (root + sublayers + session), /// NOT layers that came in through references or payloads. diff --git a/src/ui/StageEditorPanel.cpp b/src/ui/StageEditorPanel.cpp index 9a2a5fe..0e007e8 100644 --- a/src/ui/StageEditorPanel.cpp +++ b/src/ui/StageEditorPanel.cpp @@ -123,13 +123,18 @@ void StageEditorPanel::RenderFixedLayers(const std::vector& layers) { ImGui::TableSetColumnIndex(3); { + bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive(); bool isET = li.isEditTarget; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + ImGui::BeginDisabled(renderLayerActive); if (ImGui::Checkbox("##et", &isET) && isET) m_layerManager->SetEditTarget(li.identifier); + ImGui::EndDisabled(); ImGui::PopStyleVar(); if (ImGui::IsItemHovered()) - ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); + ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer" + : li.isEditTarget ? "Edit target (active)" + : "Set as edit target"); } ImGui::PopID(); @@ -242,6 +247,9 @@ void StageEditorPanel::RenderSublayerList(const std::vector& sublayer else ImGui::TextDisabled(" "); + bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer); + bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive(); + // Col 2: layer name ImGui::TableSetColumnIndex(2); ImVec4 nameColor = li.isMuted @@ -250,6 +258,12 @@ void StageEditorPanel::RenderSublayerList(const std::vector& sublayer ImGui::TextColored(nameColor, "%s", li.displayName.c_str()); if (ImGui::IsItemHovered() && !li.realPath.empty()) ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str()); + if (isRenderLayer) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Managed from the Render Layer panel"); + } // Col 3: edit target checkbox — checking sets this layer as edit target; // unchecking does nothing (there is always an active edit target). @@ -257,23 +271,32 @@ void StageEditorPanel::RenderSublayerList(const std::vector& sublayer { bool isET = li.isEditTarget; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + ImGui::BeginDisabled(renderLayerActive); if (ImGui::Checkbox("##et", &isET) && isET) m_layerManager->SetEditTarget(li.identifier); + ImGui::EndDisabled(); ImGui::PopStyleVar(); if (ImGui::IsItemHovered()) - ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); + ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer" + : li.isEditTarget ? "Edit target (active)" + : "Set as edit target"); } - // Col 4: mute checkbox + // Col 4: mute checkbox — disabled for render-layer sublayers, which + // RenderLayerManager mutes/unmutes exclusively as layers are switched. ImGui::TableSetColumnIndex(4); { bool muted = li.isMuted; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + ImGui::BeginDisabled(isRenderLayer); if (ImGui::Checkbox("##muted", &muted)) { if (muted) m_layerManager->MuteLayer(li.identifier); else m_layerManager->UnmuteLayer(li.identifier); } + ImGui::EndDisabled(); ImGui::PopStyleVar(); + if (isRenderLayer && ImGui::IsItemHovered()) + ImGui::SetTooltip("Managed from the Render Layer panel"); } ImGui::PopID(); @@ -289,14 +312,21 @@ void StageEditorPanel::RenderSublayerContextMenu(int i, return; const auto& li = sublayers[i]; + bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer); + bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive(); - if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) { + if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) { if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier); else m_layerManager->MuteLayer(li.identifier); } + if (isRenderLayer && ImGui::IsItemHovered()) + ImGui::SetTooltip("Managed from the Render Layer panel"); - if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target")) + if (!li.isEditTarget && + ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive)) m_layerManager->SetEditTarget(li.identifier); + if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered()) + ImGui::SetTooltip("Edit target is locked to the active render layer"); ImGui::Separator(); @@ -337,13 +367,15 @@ void StageEditorPanel::RenderSublayerContextMenu(int i, ImGui::Separator(); - if (ImGui::MenuItem("Remove")) { + if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) { if (m_commandHistory) m_commandHistory->Push( std::make_unique(m_layerManager, i)); else m_layerManager->RemoveSublayer(i); } + if (isRenderLayer && ImGui::IsItemHovered()) + ImGui::SetTooltip("Managed from the Render Layer panel"); ImGui::EndPopup(); } diff --git a/src/ui/StageEditorPanel.h b/src/ui/StageEditorPanel.h index e90fbf1..fe07869 100644 --- a/src/ui/StageEditorPanel.h +++ b/src/ui/StageEditorPanel.h @@ -2,6 +2,7 @@ #include "../core/LayerManager.h" #include "../core/CommandHistory.h" +#include "../core/RenderLayerManager.h" #include "IconManager.h" #include #include @@ -17,6 +18,12 @@ public: void SetLayerManager(LayerManager* manager) { m_layerManager = manager; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetIconManager(IconManager* icons) { m_iconManager = icons; } + /// Optional. When set: sublayers RenderLayerManager created are badged + /// and have their mute/remove controls disabled here (they're managed + /// exclusively from the Render Layer panel so the two can't desync), and + /// every row's edit-target checkbox is disabled while a non-Default + /// render layer is active. + void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; } void Render(); private: @@ -24,9 +31,10 @@ private: void RenderSublayerList(const std::vector& sublayers); void RenderSublayerContextMenu(int i, const std::vector& sublayers); - LayerManager* m_layerManager = nullptr; - CommandHistory* m_commandHistory = nullptr; - IconManager* m_iconManager = nullptr; + LayerManager* m_layerManager = nullptr; + CommandHistory* m_commandHistory = nullptr; + IconManager* m_iconManager = nullptr; + RenderLayerManager* m_renderLayerManager = nullptr; int m_selectedIdx = -1; };