Init Repo
|
|
@ -0,0 +1,74 @@
|
|||
# Build directories
|
||||
build/
|
||||
install/
|
||||
out/
|
||||
.kilocode/
|
||||
.kilo/
|
||||
third_party/
|
||||
example/
|
||||
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
*.cmake
|
||||
!CMakeLists.txt
|
||||
!cmake/modules/*.cmake
|
||||
!CMakePresets.json
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
*.sln
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Compiled Object files
|
||||
*.o
|
||||
*.obj
|
||||
*.lo
|
||||
*.slo
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Compiled Static libraries
|
||||
*.a
|
||||
*.lib
|
||||
*.lai
|
||||
*.la
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Debug files
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.exp
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
C++17 / Windows desktop app using **OpenUSD + ImGui + OpenGL**. USD scene editor with layer-based overrides for lights, geometry, and materials — similar to Maya Render Layers.
|
||||
|
||||
---
|
||||
|
||||
## Build Commands
|
||||
|
||||
```powershell
|
||||
# Configure (Visual Studio 17 2022, outputs to ./build, installs to ./install)
|
||||
cmake --preset default
|
||||
|
||||
# Build Release
|
||||
cmake --build build --config Release
|
||||
|
||||
# Install
|
||||
cmake --install build --config Release
|
||||
|
||||
# Build & run tests
|
||||
cmake --build build --config Release
|
||||
ctest --test-dir build -C Release
|
||||
```
|
||||
|
||||
- **Always** use `cmake --preset default` — do not invoke CMake manually with ad-hoc `-G` flags.
|
||||
- Build output: `./build/` | Install prefix: `./install/`
|
||||
- The installed `App.exe` lives in `install/bin/`.
|
||||
|
||||
---
|
||||
|
||||
## Key Paths
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| OpenUSD SDK | `third_party/OpenUSD_v25.05/` |
|
||||
| ImGui source | `third_party/imgui-1.92.7/` |
|
||||
| GLAD loader | `third_party/glad/` |
|
||||
| Inter font | `third_party/Inter/Inter.ttc` |
|
||||
| CMake Find modules | `cmake/modules/` (`FindOpenUSD.cmake`, `FindImgui.cmake`, `FindGlad.cmake`) |
|
||||
| USD business logic | `src/core/` |
|
||||
| ImGui panels | `src/ui/` |
|
||||
| Utilities | `src/utils/` |
|
||||
| Tests | `tests/` |
|
||||
| OpenSpec specs/changes | `openspec/changes/` |
|
||||
|
||||
> Note: AGENTS.md previously listed `OpenUSD_v25.08` — the actual SDK on disk is `OpenUSD_v25.05`. Trust `CMakePresets.json` / `third_party/` over prose.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **Namespace**: all project code uses `namespace UsdLayerManager`.
|
||||
- USD plugin registry: at runtime the app scans `<exe_dir>/usd/` for `plugInfo.json` and registers plugins via `pxr::PlugRegistry`.
|
||||
- Renderer uses `UsdImagingGLEngine` into a `GlfDrawTarget` FBO; viewport picking done via engine ray-cast.
|
||||
- `ViewportCamera` mirrors `usdview`'s `FreeCamera.py` (Tumble/Truck/AdjustDistance/FrameSelection); supports both Free and USD camera-prim modes.
|
||||
- ImGui docking branch is used; docking enabled via `IMGUI_HAS_DOCK`.
|
||||
- Python 3.12 (`python312.dll`) must be present — OpenUSD runtime requires it. CMake copies it to output via `POST_BUILD`.
|
||||
- Windows-only: uses `imgui_impl_win32` + `imgui_impl_opengl3`.
|
||||
|
||||
---
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **API Pre-check**: Before writing any OpenUSD API call, scan headers first:
|
||||
```powershell
|
||||
findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\
|
||||
```
|
||||
Never assume an API exists — the actual SDK version may differ from docs.
|
||||
|
||||
2. **Build before test**: Complete `cmake --build` + `cmake --install` before running `App.exe` or CTest. Tests copy USD/Python DLLs via `POST_BUILD`; skipping the build step breaks DLL resolution.
|
||||
|
||||
3. **Self-healing**: If compilation fails, extract the MSBuild error log and fix the root cause. Do not retry the same approach more than once.
|
||||
|
||||
4. **No CI**: There are no GitHub Actions workflows. Validation is manual via CTest and running `App.exe`.
|
||||
|
||||
---
|
||||
|
||||
## Feature Workflow (OpenSpec)
|
||||
|
||||
Feature proposals live under `openspec/changes/<name>/` with `proposal.md`, `design.md`, `specs/`, and `tasks.md` (checkbox-tracked). Use the `openspec-*` skills (via Kilo) to propose, implement, and archive changes.
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# CLAUDE.md — 12-rule template
|
||||
|
||||
These rules apply to every task in this project unless explicitly overridden.
|
||||
Bias: caution over speed on non-trivial work. Use judgment on trivial tasks.
|
||||
|
||||
## Rule 1 — Think Before Coding
|
||||
State assumptions explicitly. If uncertain, ask rather than guess.
|
||||
Present multiple interpretations when ambiguity exists.
|
||||
Push back when a simpler approach exists.
|
||||
Stop when confused. Name what's unclear.
|
||||
|
||||
## Rule 2 — Simplicity First
|
||||
Minimum code that solves the problem. Nothing speculative.
|
||||
No features beyond what was asked. No abstractions for single-use code.
|
||||
Test: would a senior engineer say this is overcomplicated? If yes, simplify.
|
||||
|
||||
## Rule 3 — Surgical Changes
|
||||
Touch only what you must. Clean up only your own mess.
|
||||
Don't "improve" adjacent code, comments, or formatting.
|
||||
Don't refactor what isn't broken. Match existing style.
|
||||
|
||||
## Rule 4 — Goal-Driven Execution
|
||||
Define success criteria. Loop until verified.
|
||||
Don't follow steps. Define success and iterate.
|
||||
Strong success criteria let you loop independently.
|
||||
|
||||
## Rule 5 — Use the model only for judgment calls
|
||||
Use me for: classification, drafting, summarization, extraction.
|
||||
Do NOT use me for: routing, retries, deterministic transforms.
|
||||
If code can answer, code answers.
|
||||
|
||||
## Rule 6 — Token budgets are not advisory
|
||||
Per-task: 4,000 tokens. Per-session: 30,000 tokens.
|
||||
If approaching budget, summarize and start fresh.
|
||||
Surface the breach. Do not silently overrun.
|
||||
|
||||
## Rule 7 — Surface conflicts, don't average them
|
||||
If two patterns contradict, pick one (more recent / more tested).
|
||||
Explain why. Flag the other for cleanup.
|
||||
Don't blend conflicting patterns.
|
||||
|
||||
## Rule 8 — Read before you write
|
||||
Before adding code, read exports, immediate callers, shared utilities.
|
||||
"Looks orthogonal" is dangerous. If unsure why code is structured a way, ask.
|
||||
|
||||
## Rule 9 — Tests verify intent, not just behavior
|
||||
Tests must encode WHY behavior matters, not just WHAT it does.
|
||||
A test that can't fail when business logic changes is wrong.
|
||||
|
||||
## Rule 10 — Checkpoint after every significant step
|
||||
Summarize what was done, what's verified, what's left.
|
||||
Don't continue from a state you can't describe back.
|
||||
If you lose track, stop and restate.
|
||||
|
||||
## Rule 11 — Match the codebase's conventions, even if you disagree
|
||||
Conformance > taste inside the codebase.
|
||||
If you genuinely think a convention is harmful, surface it. Don't fork silently.
|
||||
|
||||
## Rule 12 — Fail loud
|
||||
"Completed" is wrong if anything was skipped silently.
|
||||
"Tests pass" is wrong if any were skipped.
|
||||
Default to surfacing uncertainty, not hiding it.
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
project(UsdLayerManager VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
# Set C++ standard
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Add cmake modules path
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
|
||||
|
||||
# Find dependencies
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(OpenUSD REQUIRED)
|
||||
find_package(Imgui REQUIRED)
|
||||
find_package(Glad REQUIRED)
|
||||
|
||||
# Collect source files
|
||||
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
|
||||
file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp")
|
||||
file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp")
|
||||
set(MAIN_SOURCE "src/main.cpp")
|
||||
|
||||
# Create executable
|
||||
add_executable(UsdLayerManager
|
||||
${MAIN_SOURCE}
|
||||
${CORE_SOURCES}
|
||||
${UI_SOURCES}
|
||||
${UTILS_SOURCES}
|
||||
)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(UsdLayerManager PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/fontawesome
|
||||
${CMAKE_SOURCE_DIR}/third_party/nanosvg
|
||||
)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(UsdLayerManager PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Imgui::Imgui
|
||||
Glad::Glad
|
||||
)
|
||||
|
||||
# Platform-specific settings
|
||||
if(WIN32)
|
||||
# Windows-specific settings
|
||||
target_compile_definitions(UsdLayerManager PRIVATE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
NOMINMAX
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
)
|
||||
|
||||
# Copy runtime DLLs to output directory for each configuration
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
# OpenUSD lib/ — usd_*.dll, boost_*.dll, Alembic.dll, etc.
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_BIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
# OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc.
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/bin"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/lib/usd"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/plugin/usd"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying runtime DLLs and USD plugins..."
|
||||
)
|
||||
endif()
|
||||
|
||||
# Compiler warnings
|
||||
if(MSVC)
|
||||
target_compile_options(UsdLayerManager PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(UsdLayerManager PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
# Install rules
|
||||
install(TARGETS UsdLayerManager
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
# Install all OpenUSD runtime DLLs (same set that POST_BUILD copies to build/Release).
|
||||
# Without these the exe cannot start from install/bin.
|
||||
install(DIRECTORY ${OpenUSD_BIN_DIR}/
|
||||
DESTINATION bin
|
||||
FILES_MATCHING PATTERN "*.dll"
|
||||
)
|
||||
|
||||
# Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR,
|
||||
# OpenImageIO, zlib, etc. These are separate from lib/ and also required at runtime.
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/
|
||||
DESTINATION bin
|
||||
FILES_MATCHING PATTERN "*.dll"
|
||||
)
|
||||
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd
|
||||
DESTINATION bin
|
||||
)
|
||||
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd
|
||||
DESTINATION bin/usd
|
||||
)
|
||||
|
||||
install(FILES ${OpenUSD_ROOT_DIR}/plugin/usd/hdStorm.dll
|
||||
DESTINATION bin
|
||||
)
|
||||
|
||||
# Install Python runtime DLLs required by OpenUSD.
|
||||
install(FILES
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||
DESTINATION bin
|
||||
OPTIONAL # OPTIONAL so the install doesn't fail on machines with a
|
||||
# system-wide Python; in that case the DLL is on PATH already.
|
||||
)
|
||||
|
||||
# Copy resources to build directory
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/resources
|
||||
DESTINATION ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
|
||||
# Copy font and SVG icons to build directory
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>/../resources/font"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/font/Inter.ttc"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/icons"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
|
||||
COMMENT "Copying fonts and SVG icons to build output"
|
||||
)
|
||||
|
||||
# Install resources (icons, fonts placeholder) next to the exe so the
|
||||
# exe-relative path "resources/..." resolves correctly from install/bin/.
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
|
||||
DESTINATION bin
|
||||
)
|
||||
|
||||
# Install Inter font next to the exe under resources/font/
|
||||
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
|
||||
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
|
||||
DESTINATION bin/resources/font
|
||||
)
|
||||
|
||||
# Print configuration summary
|
||||
message(STATUS "")
|
||||
message(STATUS "=== USD Layer Manager Configuration ===")
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")
|
||||
message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
|
||||
message(STATUS "OpenUSD found: ${OpenUSD_FOUND}")
|
||||
message(STATUS "ImGui found: ${Imgui_FOUND}")
|
||||
message(STATUS "OpenGL found: ${OPENGL_FOUND}")
|
||||
message(STATUS "Glad found: ${Glad_FOUND}")
|
||||
message(STATUS "Build tests: ${BUILD_TESTS}")
|
||||
message(STATUS "=======================================")
|
||||
message(STATUS "")
|
||||
|
||||
# === Test build option ===
|
||||
option(BUILD_TESTS "Build test executables" OFF)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
|
||||
# === Viewport Display Test ===
|
||||
add_executable(ViewportDisplayTest
|
||||
tests/viewport_display_test.cpp
|
||||
${CORE_SOURCES}
|
||||
${UTILS_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(ViewportDisplayTest PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(ViewportDisplayTest PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Glad::Glad
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_compile_definitions(ViewportDisplayTest PRIVATE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
NOMINMAX
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
)
|
||||
|
||||
add_custom_command(TARGET ViewportDisplayTest POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_BIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:ViewportDisplayTest>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/lib/usd"
|
||||
"$<TARGET_FILE_DIR:ViewportDisplayTest>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/plugin/usd"
|
||||
"$<TARGET_FILE_DIR:ViewportDisplayTest>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||
"$<TARGET_FILE_DIR:ViewportDisplayTest>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||
"$<TARGET_FILE_DIR:ViewportDisplayTest>"
|
||||
COMMENT "Copying runtime DLLs and USD plugins for test..."
|
||||
)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME ViewportDisplayTest COMMAND ViewportDisplayTest)
|
||||
|
||||
add_executable(RendererDiagnosticTest
|
||||
tests/renderer_diagnostic_test.cpp
|
||||
${CORE_SOURCES}
|
||||
${UTILS_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(RendererDiagnosticTest PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(RendererDiagnosticTest PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Glad::Glad
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_compile_definitions(RendererDiagnosticTest PRIVATE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
NOMINMAX
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
)
|
||||
|
||||
add_custom_command(TARGET RendererDiagnosticTest POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_BIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:RendererDiagnosticTest>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/lib/usd"
|
||||
"$<TARGET_FILE_DIR:RendererDiagnosticTest>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/plugin/usd"
|
||||
"$<TARGET_FILE_DIR:RendererDiagnosticTest>/usd"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||
"$<TARGET_FILE_DIR:RendererDiagnosticTest>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||
"$<TARGET_FILE_DIR:RendererDiagnosticTest>"
|
||||
COMMENT "Copying runtime DLLs and USD plugins for diagnostic test..."
|
||||
)
|
||||
endif()
|
||||
|
||||
endif() # BUILD_TESTS
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"version": 3,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "Default Configuration",
|
||||
"description": "Default build configuration for USD Layer Manager",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
|
||||
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD_v25.05",
|
||||
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
|
||||
"CMAKE_CXX_STANDARD": "17",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"BUILD_TESTS": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"displayName": "Debug Configuration",
|
||||
"description": "Debug build with symbols",
|
||||
"inherits": "default",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug",
|
||||
"BUILD_TESTS": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"displayName": "Release Configuration",
|
||||
"description": "Optimized release build",
|
||||
"inherits": "default",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"BUILD_TESTS": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no-tests",
|
||||
"displayName": "No Tests Configuration",
|
||||
"description": "Release build without test executables",
|
||||
"inherits": "default",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"BUILD_TESTS": "OFF"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "Default Build",
|
||||
"configurePreset": "default",
|
||||
"configuration": "Release"
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"displayName": "Debug Build",
|
||||
"configurePreset": "debug",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"displayName": "Release Build",
|
||||
"configurePreset": "release",
|
||||
"configuration": "Release"
|
||||
},
|
||||
{
|
||||
"name": "no-tests",
|
||||
"displayName": "No Tests Build",
|
||||
"configurePreset": "no-tests",
|
||||
"configuration": "Release"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# USD Layer Manager
|
||||
|
||||
A C++17 / Windows desktop application providing a **Maya Render Layers-style** USD scene editor using **OpenUSD v25.05**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, and interact with 3D scenes through a Hydra viewport.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Layer Stack Management** — Create, reorder, mute/unmute sublayers with full undo support
|
||||
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename prims; add/replace references
|
||||
- **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms
|
||||
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates; grid overlay; selection bounding boxes; camera wireframes
|
||||
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection
|
||||
- **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space
|
||||
- **Undo/Redo** — Full command history for prim creation/deletion, attribute edits, transform changes, and layer operations
|
||||
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Version | Notes |
|
||||
|---|---|---|
|
||||
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.05 | Prebuilt; found via `FindOpenUSD.cmake` |
|
||||
| [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends |
|
||||
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
|
||||
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
|
||||
|
||||
## Build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Visual Studio 17 2022 (MSVC v143) with C++17 support
|
||||
- CMake 3.20+
|
||||
- Python 3.12 installed at `%LOCALAPPDATA%\Programs\Python\Python312\`
|
||||
|
||||
### Steps
|
||||
|
||||
```powershell
|
||||
cmake --preset default
|
||||
cmake --build build --config Release
|
||||
cmake --install build --config Release
|
||||
```
|
||||
|
||||
The installed `App.exe` lives in `install/bin/`.
|
||||
|
||||
**Presets** — `default` (Release + tests), `debug`, `release`, `no-tests`.
|
||||
|
||||
### Tests
|
||||
|
||||
```powershell
|
||||
cmake --build build --config Release
|
||||
ctest --test-dir build -C Release
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.cpp — Entry point (plugin path init, app lifecycle)
|
||||
├── core/ — USD business logic (no UI dependency)
|
||||
│ ├── UsdStageManager — Stage open/create/save/close lifecycle
|
||||
│ ├── LayerManager — Layer stack introspection and mutation
|
||||
│ ├── PropertyManager — Property read/write via edit target
|
||||
│ ├── CommandHistory — Undo/redo stack for ICommand instances
|
||||
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays
|
||||
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera
|
||||
│ └── commands/ — ICommand subclasses for all mutable operations
|
||||
├── ui/ — ImGui panels and application shell
|
||||
│ ├── Application — App shell (owns managers/panels, docking, menus)
|
||||
│ ├── ImGuiContext — Win32+OpenGL+ImGui initialization and loop
|
||||
│ ├── ViewportPanel — 3D viewport with toolbar, picking, camera
|
||||
│ ├── SceneHierarchyPanel — Prim tree browser with context menus
|
||||
│ ├── PropertyPanel — Attribute/transform editor (channel box-style)
|
||||
│ ├── LayerPanel — Layer stack editor with modal dialogs
|
||||
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
|
||||
│ └── IconManager — SVG icon rasterization via NanoSVG
|
||||
└── utils/ — Cross-cutting utilities
|
||||
├── Logger — Thread-safe logging (file + console)
|
||||
├── FileDialog — Win32 file open/save dialogs
|
||||
├── PathUtils — Exe-relative path resolution
|
||||
└── GLExt — GLAD extension initialization
|
||||
```
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
- **Command Pattern** — All mutable USD operations go through `ICommand` → `CommandHistory` for undo/redo
|
||||
- **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic
|
||||
- **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update)
|
||||
- **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime
|
||||
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
|
||||
|
||||
## Project Configuration
|
||||
|
||||
| Config | Purpose |
|
||||
|---|---|
|
||||
| `cmake/modules/FindOpenUSD.cmake` | OpenUSD SDK discovery |
|
||||
| `cmake/modules/FindImgui.cmake` | ImGui source integration |
|
||||
| `cmake/modules/FindGlad.cmake` | GLAD loader setup |
|
||||
| `CMakePresets.json` | Build presets (VS 17 2022, x64) |
|
||||
| `AGENTS.md` | AI agent build/architecture guide |
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# FindGlad.cmake - Find or configure the GLAD OpenGL loader
|
||||
#
|
||||
# This module looks for glad in the third_party/glad directory (pre-generated).
|
||||
# It creates a static library target glad::glad.
|
||||
#
|
||||
# Inputs:
|
||||
# GLAD_DIR - Path to glad root (contains include/ and src/)
|
||||
#
|
||||
# Outputs:
|
||||
# Glad_FOUND
|
||||
# Glad::Glad (imported INTERFACE/STATIC target)
|
||||
|
||||
if(NOT GLAD_DIR)
|
||||
set(GLAD_DIR "${CMAKE_SOURCE_DIR}/third_party/glad")
|
||||
endif()
|
||||
|
||||
find_path(GLAD_INCLUDE_DIR
|
||||
NAMES glad/gl.h
|
||||
PATHS "${GLAD_DIR}/include"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
find_file(GLAD_SOURCE
|
||||
NAMES gl.c
|
||||
PATHS "${GLAD_DIR}/src"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Glad
|
||||
REQUIRED_VARS GLAD_INCLUDE_DIR GLAD_SOURCE
|
||||
)
|
||||
|
||||
if(Glad_FOUND AND NOT TARGET Glad::Glad)
|
||||
add_library(glad_impl STATIC "${GLAD_SOURCE}")
|
||||
|
||||
# The project enables only CXX; tell CMake that glad's C source
|
||||
# should be compiled as C and linked with the C linker.
|
||||
set_source_files_properties("${GLAD_SOURCE}" PROPERTIES LANGUAGE C)
|
||||
set_target_properties(glad_impl PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
target_include_directories(glad_impl PUBLIC "${GLAD_INCLUDE_DIR}")
|
||||
# glad needs opengl32 on Windows
|
||||
if(WIN32)
|
||||
target_link_libraries(glad_impl PUBLIC opengl32)
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
target_link_libraries(glad_impl PUBLIC OpenGL::GL)
|
||||
endif()
|
||||
|
||||
add_library(Glad::Glad ALIAS glad_impl)
|
||||
message(STATUS "Glad found: ${GLAD_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# FindImgui.cmake
|
||||
# Finds ImGui library and sets up compilation
|
||||
#
|
||||
# This module defines:
|
||||
# Imgui_FOUND - System has ImGui
|
||||
# Imgui_INCLUDE_DIRS - ImGui include directories
|
||||
# Imgui_SOURCES - ImGui source files to compile
|
||||
# Imgui::Imgui - Imported target for ImGui
|
||||
|
||||
# Find ImGui directory
|
||||
find_path(Imgui_DIR
|
||||
NAMES imgui.h
|
||||
PATHS
|
||||
${IMGUI_DIR}
|
||||
${CMAKE_SOURCE_DIR}/third_party/imgui-1.92.7
|
||||
${CMAKE_SOURCE_DIR}/third_party/imgui
|
||||
DOC "ImGui root directory"
|
||||
)
|
||||
|
||||
if(Imgui_DIR)
|
||||
set(Imgui_INCLUDE_DIRS
|
||||
${Imgui_DIR}
|
||||
${Imgui_DIR}/backends
|
||||
)
|
||||
|
||||
# Core ImGui source files
|
||||
set(Imgui_SOURCES
|
||||
${Imgui_DIR}/imgui.cpp
|
||||
${Imgui_DIR}/imgui_draw.cpp
|
||||
${Imgui_DIR}/imgui_tables.cpp
|
||||
${Imgui_DIR}/imgui_widgets.cpp
|
||||
${Imgui_DIR}/imgui_demo.cpp
|
||||
)
|
||||
|
||||
# Backend source files for Win32 + OpenGL3
|
||||
list(APPEND Imgui_SOURCES
|
||||
${Imgui_DIR}/backends/imgui_impl_win32.cpp
|
||||
${Imgui_DIR}/backends/imgui_impl_opengl3.cpp
|
||||
)
|
||||
|
||||
set(Imgui_FOUND TRUE)
|
||||
endif()
|
||||
|
||||
# Handle standard find_package arguments
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Imgui
|
||||
REQUIRED_VARS
|
||||
Imgui_DIR
|
||||
Imgui_INCLUDE_DIRS
|
||||
Imgui_SOURCES
|
||||
)
|
||||
|
||||
if(Imgui_FOUND)
|
||||
# Create imported target
|
||||
if(NOT TARGET Imgui::Imgui)
|
||||
# Create the actual library target first
|
||||
add_library(imgui_lib STATIC ${Imgui_SOURCES})
|
||||
target_include_directories(imgui_lib PUBLIC ${Imgui_INCLUDE_DIRS})
|
||||
|
||||
# Link against Windows libraries for Win32 backend and OpenGL
|
||||
target_link_libraries(imgui_lib PUBLIC
|
||||
opengl32
|
||||
gdi32
|
||||
dwmapi
|
||||
imm32
|
||||
)
|
||||
|
||||
# Enable docking support
|
||||
# target_compile_definitions(imgui_lib PUBLIC IMGUI_HAS_DOCK)
|
||||
|
||||
# Set C++ standard
|
||||
target_compile_features(imgui_lib PUBLIC cxx_std_17)
|
||||
|
||||
# Create an alias with the :: namespace
|
||||
add_library(Imgui::Imgui ALIAS imgui_lib)
|
||||
endif()
|
||||
|
||||
message(STATUS "Found ImGui: ${Imgui_DIR}")
|
||||
endif()
|
||||
|
||||
mark_as_advanced(
|
||||
Imgui_DIR
|
||||
)
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
# FindOpenUSD.cmake
|
||||
# Finds OpenUSD installation and sets up necessary variables
|
||||
#
|
||||
# This module defines:
|
||||
# OpenUSD_FOUND - System has OpenUSD
|
||||
# OpenUSD_INCLUDE_DIRS - OpenUSD include directories
|
||||
# OpenUSD_LIBRARIES - Libraries needed to use OpenUSD
|
||||
# OpenUSD_LIBRARY_DIR - Directory containing OpenUSD libraries
|
||||
# OpenUSD_VERSION - Version of OpenUSD found
|
||||
|
||||
# Look for OpenUSD root
|
||||
find_path(OpenUSD_ROOT_DIR
|
||||
NAMES include/pxr/pxr.h lib/usd_usd.lib
|
||||
PATHS
|
||||
${CMAKE_PREFIX_PATH}
|
||||
$ENV{OpenUSD_ROOT}
|
||||
DOC "OpenUSD root directory"
|
||||
)
|
||||
|
||||
if(OpenUSD_ROOT_DIR)
|
||||
set(OpenUSD_INCLUDE_DIR "${OpenUSD_ROOT_DIR}/include")
|
||||
set(OpenUSD_LIBRARY_DIR "${OpenUSD_ROOT_DIR}/lib")
|
||||
set(OpenUSD_BIN_DIR "${OpenUSD_ROOT_DIR}/lib")
|
||||
endif()
|
||||
|
||||
# Find Python 3.12 (required by this USD build)
|
||||
find_path(Python312_INCLUDE_DIR
|
||||
NAMES Python.h
|
||||
PATHS
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/include"
|
||||
"C:/Python312/include"
|
||||
"C:/Program Files/Python312/include"
|
||||
DOC "Python 3.12 include directory"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
find_path(Python312_LIBRARY_DIR
|
||||
NAMES python312.lib
|
||||
PATHS
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/libs"
|
||||
"C:/Python312/libs"
|
||||
"C:/Program Files/Python312/libs"
|
||||
DOC "Python 3.12 library directory"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
# Core USD libraries needed for the project
|
||||
set(OpenUSD_REQUIRED_LIBS
|
||||
arch
|
||||
tf
|
||||
gf
|
||||
vt
|
||||
work
|
||||
plug
|
||||
trace
|
||||
sdf
|
||||
pcp
|
||||
usd
|
||||
usdGeom
|
||||
usdLux
|
||||
usdShade
|
||||
usdImaging
|
||||
usdImagingGL
|
||||
hdx
|
||||
hd
|
||||
hdSt
|
||||
hf
|
||||
hgi
|
||||
hgiGL
|
||||
glf
|
||||
hio
|
||||
ar
|
||||
kind
|
||||
ndr
|
||||
sdr
|
||||
python
|
||||
cameraUtil
|
||||
)
|
||||
|
||||
# Dependency libraries bundled with USD
|
||||
set(OpenUSD_DEP_LIBS
|
||||
tbb
|
||||
tbbmalloc
|
||||
Imath-3_1
|
||||
osdCPU
|
||||
)
|
||||
|
||||
# Find USD libraries
|
||||
set(OpenUSD_LIBRARIES "")
|
||||
foreach(lib ${OpenUSD_REQUIRED_LIBS})
|
||||
find_library(OpenUSD_${lib}_LIBRARY
|
||||
NAMES usd_${lib}
|
||||
PATHS ${OpenUSD_LIBRARY_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
if(OpenUSD_${lib}_LIBRARY)
|
||||
list(APPEND OpenUSD_LIBRARIES ${OpenUSD_${lib}_LIBRARY})
|
||||
else()
|
||||
message(WARNING "Could not find OpenUSD library: usd_${lib}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Find dependency libraries
|
||||
foreach(lib ${OpenUSD_DEP_LIBS})
|
||||
find_library(OpenUSD_dep_${lib}_LIBRARY
|
||||
NAMES ${lib}
|
||||
PATHS ${OpenUSD_LIBRARY_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
if(OpenUSD_dep_${lib}_LIBRARY)
|
||||
list(APPEND OpenUSD_LIBRARIES ${OpenUSD_dep_${lib}_LIBRARY})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Find Python library
|
||||
if(Python312_LIBRARY_DIR)
|
||||
find_library(Python312_LIBRARY
|
||||
NAMES python312
|
||||
PATHS ${Python312_LIBRARY_DIR}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
if(Python312_LIBRARY)
|
||||
list(APPEND OpenUSD_LIBRARIES ${Python312_LIBRARY})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Try to find version from pxr.h
|
||||
if(OpenUSD_INCLUDE_DIR AND EXISTS "${OpenUSD_INCLUDE_DIR}/pxr/pxr.h")
|
||||
file(STRINGS "${OpenUSD_INCLUDE_DIR}/pxr/pxr.h" _version_line
|
||||
REGEX "^#define PXR_VERSION ")
|
||||
if(_version_line)
|
||||
string(REGEX REPLACE "^#define PXR_VERSION ([0-9]+).*" "\\1" OpenUSD_VERSION "${_version_line}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Handle standard find_package arguments
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(OpenUSD
|
||||
REQUIRED_VARS
|
||||
OpenUSD_INCLUDE_DIR
|
||||
OpenUSD_LIBRARY_DIR
|
||||
OpenUSD_LIBRARIES
|
||||
VERSION_VAR OpenUSD_VERSION
|
||||
)
|
||||
|
||||
if(OpenUSD_FOUND)
|
||||
set(OpenUSD_INCLUDE_DIRS
|
||||
${OpenUSD_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
# Add Python include if found
|
||||
if(Python312_INCLUDE_DIR)
|
||||
list(APPEND OpenUSD_INCLUDE_DIRS ${Python312_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
# Create imported target
|
||||
if(NOT TARGET OpenUSD::OpenUSD)
|
||||
add_library(OpenUSD::OpenUSD INTERFACE IMPORTED)
|
||||
set_target_properties(OpenUSD::OpenUSD PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${OpenUSD_INCLUDE_DIRS}"
|
||||
INTERFACE_LINK_LIBRARIES "${OpenUSD_LIBRARIES}"
|
||||
)
|
||||
|
||||
# Add required compile definitions for USD
|
||||
target_compile_definitions(OpenUSD::OpenUSD INTERFACE
|
||||
NOMINMAX
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Found OpenUSD: ${OpenUSD_INCLUDE_DIR}")
|
||||
message(STATUS "OpenUSD Libraries: ${OpenUSD_LIBRARY_DIR}")
|
||||
message(STATUS "OpenUSD Binaries: ${OpenUSD_BIN_DIR}")
|
||||
if(Python312_INCLUDE_DIR)
|
||||
message(STATUS "Python 3.12: ${Python312_INCLUDE_DIR}")
|
||||
endif()
|
||||
if(OpenUSD_VERSION)
|
||||
message(STATUS "OpenUSD Version: ${OpenUSD_VERSION}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(
|
||||
OpenUSD_INCLUDE_DIR
|
||||
OpenUSD_LIBRARY_DIR
|
||||
Python312_INCLUDE_DIR
|
||||
Python312_LIBRARY_DIR
|
||||
)
|
||||
|
After Width: | Height: | Size: 345 KiB |
|
|
@ -0,0 +1,33 @@
|
|||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,19
|
||||
Size=1264,662
|
||||
Collapsed=0
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Scene Hierarchy]
|
||||
Pos=60,60
|
||||
Size=290,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][Layer Panel]
|
||||
Pos=60,60
|
||||
Size=121,48
|
||||
Collapsed=0
|
||||
|
||||
[Window][Viewport]
|
||||
Pos=60,60
|
||||
Size=797,60
|
||||
Collapsed=0
|
||||
|
||||
[Window][Property Panel]
|
||||
Pos=60,60
|
||||
Size=121,48
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,19 Size=1264,662 CentralNode=1
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
## Context
|
||||
|
||||
The application currently uses a fixed-position, fixed-size layout for all ImGui panels. Each window is placed via `SetNextWindowPos`/`SetNextWindowSize` with `ImGuiCond_FirstUseEver`. The `imconfig.h` file has docking-related macros fully commented out, and `ImGuiContext::Initialize()` only sets `ImGuiConfigFlags_NavEnableKeyboard`. A comment in the code explicitly states that docking requires a special build configuration that was intentionally deferred.
|
||||
|
||||
ImGui's docking branch (`docking` in the imgui repo) has been stable for years and is widely used. The prebuilt ImGui source in `third_party/Imgui-1.92.7` may or may not include docking support — this must be verified.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Enable ImGui docking at compile time and runtime
|
||||
- Create a full-window dockspace spanning the entire application viewport
|
||||
- All existing panels (Scene Hierarchy, Layer Panel, Viewport, Property Panel, Stage Info) become dockable
|
||||
- Layout persists across sessions via `imgui.ini`
|
||||
- Window visibility toggles continue to work via the View menu
|
||||
|
||||
**Non-Goals:**
|
||||
- Multi-viewport support (floating OS windows) — this requires additional `ImGuiConfigFlags_ViewportsEnable` and is not needed
|
||||
- Custom docking layout presets or saved workspace files
|
||||
- Theming or styling changes beyond docking enablement
|
||||
- Refactoring the panel class hierarchy — panels keep their existing `Render()` signatures
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Use `ImGui::DockSpaceOverViewport` over manual `DockSpace`
|
||||
|
||||
**Rationale**: `DockSpaceOverViewport()` automatically creates a dockspace that fills the main viewport and handles resizing. A manual `DockSpace()` requires managing the dockspace ID and the host window's position/size manually. For a single-window application, `DockSpaceOverViewport` is simpler and sufficient.
|
||||
|
||||
**Alternative considered**: Manual `ImGui::DockSpace(ImGui::GetID("MainDockSpace"))` inside a full-viewport `ImGui::Begin/End` block. Rejected because it requires more boilerplate with no benefit for our use case.
|
||||
|
||||
### 2. Enable docking via CMake compile definition, not imconfig.h
|
||||
|
||||
**Rationale**: Adding `target_compile_definitions(... PRIVATE IMGUI_HAS_DOCK)` in `FindImgui.cmake` keeps the configuration centralized and avoids modifying third-party header files. This is the standard approach for conditionally enabling ImGui features.
|
||||
|
||||
**Alternative considered**: Modifying `imconfig.h` to uncomment `#define IMGUI_HAS_DOCK`. Rejected because it modifies third-party source and would be lost on imgui updates.
|
||||
|
||||
### 3. Remove `SetNextWindowPos`/`SetNextWindowSize` entirely
|
||||
|
||||
**Rationale**: When docking is enabled, ImGui manages window positions. Keeping `SetNextWindowPos` calls could interfere with docking behavior. Windows will be created as dockable by default, and the docking system handles placement.
|
||||
|
||||
**Alternative considered**: Keeping `SetNextWindowPos` with `ImGuiCond_FirstUseEver` as fallback for undocked state. Rejected because it adds complexity with no user benefit — the docking system's layout persistence already handles first-use defaults.
|
||||
|
||||
### 4. Keep `ImGuiWindowFlags_NoCollapse` on main panels
|
||||
|
||||
**Rationale**: Core tool panels (Scene Hierarchy, Layer Panel, Viewport, Property Panel) should always be accessible. Allowing collapse could leave users with no visible panel. The View menu already provides show/hide toggles for optional panels.
|
||||
|
||||
**Alternative considered**: Allowing collapse on all windows. Rejected because it's easy for users to lose core panels with no obvious way to restore them.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Risk**: The prebuilt imgui library in `third_party/Imgui-1.92.7` may not include the docking branch source. → **Mitigation**: Verify the imgui source files contain `DockSpaceOverViewport` and related docking APIs. If not, the docking branch must be fetched.
|
||||
- **Risk**: `imgui.ini` may accumulate stale docking layout data across builds/versions. → **Mitigation**: This is standard ImGui behavior; users can reset by deleting `imgui.ini`. Not a new problem.
|
||||
- **Risk**: Docking increases per-frame CPU overhead slightly due to dock node management. → **Mitigation**: Negligible for the number of windows in this application (5-6 panels). No measurable performance impact expected.
|
||||
- **Risk**: The current legacy WGL/OpenGL setup uses `#version 130` which is compatible with docking. → **Mitigation**: Docking operates at the ImGui level (vertex data generation), not at the rendering backend level. No OpenGL version concerns.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## Why
|
||||
|
||||
All ImGui windows are fixed-position, fixed-size, and non-dockable, forcing users into a rigid layout that cannot adapt to different screen sizes, workflows, or preferences. Enabling ImGui's built-in docking system allows users to freely arrange, resize, tab, and float panels to suit their workflow — a critical UX feature for any editor application.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Enable ImGui docking by defining `IMGUI_HAS_DOCK` at compile time via CMake
|
||||
- Set `ImGuiConfigFlags_DockingEnable` in the ImGui IO config flags
|
||||
- Create a full-window dockspace in the main render loop
|
||||
- Replace hardcoded `SetNextWindowPos`/`SetNextWindowSize` calls with dockable window creation
|
||||
- Remove manual positioning logic for all panels (Scene Hierarchy, Layer Panel, Viewport, Property Panel, Stage Info)
|
||||
- Preserve window visibility toggles (Stage Info, Demo Window) via the View menu
|
||||
- Ensure dock layout persists across sessions via `imgui.ini`
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `imgui-docking`: Main application dock space with draggable, resizable, and tabbable panels. Includes compile-time docking enablement, runtime dockspace creation, and panel migration from fixed-position to dock-based layout.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing specs to modify -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **CMake**: `FindImgui.cmake` — add `IMGUI_HAS_DOCK` compile definition
|
||||
- **imconfig.h**: May need to ensure docking macros are uncommented (or rely on CMake define)
|
||||
- **ImGuiContext.cpp**: Add `ImGuiConfigFlags_DockingEnable` to IO config flags in `Initialize()`
|
||||
- **Application.cpp**: Add dockspace creation at the start of `RenderUI()`, remove all `SetNextWindowPos`/`SetNextWindowSize` calls from panel rendering
|
||||
- **All panel windows**: No API changes — windows remain compatible with docking as-is
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Docking is enabled at compile time
|
||||
|
||||
The build system SHALL define `IMGUI_HAS_DOCK` prior to compiling any translation unit that includes `imgui.h`.
|
||||
|
||||
#### Scenario: ImGui compiled with docking support
|
||||
- **WHEN** the project is compiled with CMake
|
||||
- **THEN** `IMGUI_HAS_DOCK` is defined for all imgui source files and any file including `imgui.h`
|
||||
|
||||
### Requirement: Docking is enabled at runtime
|
||||
|
||||
The application SHALL set `ImGuiConfigFlags_DockingEnable` in the ImGui IO configuration during initialization.
|
||||
|
||||
#### Scenario: Docking flag set on startup
|
||||
- **WHEN** the application starts and `ImGuiContext::Initialize()` is called
|
||||
- **THEN** `io.ConfigFlags` includes `ImGuiConfigFlags_DockingEnable`
|
||||
|
||||
### Requirement: Main dockspace spans the application window
|
||||
|
||||
The application SHALL create a full-viewport dockspace via `ImGui::DockSpaceOverViewport()` at the start of each frame's UI rendering.
|
||||
|
||||
#### Scenario: Dockspace created each frame
|
||||
- **WHEN** `Application::RenderUI()` executes
|
||||
- **THEN** `ImGui::DockSpaceOverViewport()` is called after `NewFrame()` and before any panel `Begin/End` calls
|
||||
|
||||
#### Scenario: Dockspace fills the entire viewport
|
||||
- **WHEN** the application window is resized
|
||||
- **THEN** the dockspace automatically fills the new window dimensions
|
||||
|
||||
### Requirement: All panels are dockable
|
||||
|
||||
All application panels SHALL be created as dockable ImGui windows that integrate with the dockspace.
|
||||
|
||||
#### Scenario: Panel windows use standard Begin/End
|
||||
- **WHEN** any panel window is rendered
|
||||
- **THEN** its `ImGui::Begin()` call does not include `SetNextWindowPos` or `SetNextWindowSize` calls
|
||||
- **AND** the window can be dragged, docked, tabbed, or floated by the user
|
||||
|
||||
#### Scenario: Core panels cannot be collapsed
|
||||
- **WHEN** the Scene Hierarchy, Layer Panel, Viewport, or Property Panel windows are rendered
|
||||
- **THEN** each window is created with `ImGuiWindowFlags_NoCollapse`
|
||||
|
||||
### Requirement: Window visibility toggles persist
|
||||
|
||||
Existing View menu toggles for Stage Info and Demo Window SHALL continue to control window visibility.
|
||||
|
||||
#### Scenario: Toggle Stage Info visibility
|
||||
- **WHEN** the user clicks "Stage Info" in the View menu
|
||||
- **THEN** the Stage Info window appears or disappears
|
||||
- **AND** the toggle state is reflected by the checkmark in the menu item
|
||||
|
||||
#### Scenario: Toggle Demo Window visibility
|
||||
- **WHEN** the user clicks "ImGui Demo" in the View menu
|
||||
- **THEN** the ImGui Demo Window appears or disappears
|
||||
- **AND** the toggle state is reflected by the checkmark in the menu item
|
||||
|
||||
### Requirement: Dock layout persists across sessions
|
||||
|
||||
The docking layout SHALL be saved to and restored from `imgui.ini` automatically by ImGui's built-in persistence.
|
||||
|
||||
#### Scenario: Layout restored on restart
|
||||
- **WHEN** the user arranges panels into a custom docking layout and restarts the application
|
||||
- **THEN** the previous docking layout is restored
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
## 1. Build Configuration
|
||||
|
||||
- [x] 1.1 Verify that `third_party/Imgui-1.92.7` source files contain docking APIs (`DockSpaceOverViewport`, `ImGuiConfigFlags_DockingEnable`). If not, fetch the docking branch of imgui.
|
||||
- [x] 1.2 Add `IMGUI_HAS_DOCK` compile definition in `cmake/modules/FindImgui.cmake` via `target_compile_definitions` on the imgui library target.
|
||||
|
||||
## 2. Runtime Docking Enablement
|
||||
|
||||
- [x] 2.1 In `src/ui/ImGuiContext.cpp`, add `ImGuiConfigFlags_DockingEnable` to `io.ConfigFlags` in the `Initialize()` method.
|
||||
- [x] 2.2 In `src/ui/Application.cpp`, add `ImGui::DockSpaceOverViewport()` call in `RenderUI()` after `NewFrame()` and before any window/panel `Begin/End` calls.
|
||||
|
||||
## 3. Panel Migration to Dockable Layout
|
||||
|
||||
- [x] 3.1 Remove all `ImGui::SetNextWindowPos()` and `ImGui::SetNextWindowSize()` calls from every panel window in `Application::RenderUI()` (Scene Hierarchy, Layer Panel, Viewport, Property Panel, Stage Info, Demo Window).
|
||||
- [x] 3.2 Add `ImGuiWindowFlags_NoCollapse` to the four core panels: Scene Hierarchy, Layer Panel, Viewport, Property Panel.
|
||||
- [x] 3.3 Verify that the View menu visibility toggles (Stage Info, Demo Window) continue to work with dockable windows.
|
||||
|
||||
## 4. Build and Verification
|
||||
|
||||
- [x] 4.1 Run `cmake --preset default` and `cmake --build` to verify the project compiles without errors.
|
||||
- [ ] 4.2 Launch `App.exe` and confirm: dockspace fills the window, panels are draggable/dockable/tabbable, layout persists after restart (check `imgui.ini`), and View menu toggles work correctly.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-25
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
## Context
|
||||
|
||||
The renderer (`UsdSceneRenderer`) already has a working pattern for GL line overlays drawn into the FBO: a shared GLSL program (`#version 130`, `in vec3 position; uniform mat4 mvpMatrix; uniform vec4 color`) is compiled once and used by both `DrawAxis` and `DrawBoundingBoxes`. The FBO can be re-bound at any time via `BindDrawTarget()`/`UnbindDrawTarget()`. The `TransformManipulator` already selects and edits any `Xformable` prim — no changes to it are needed.
|
||||
|
||||
`UsdGeomCamera` inherits from `UsdGeomXformable`. Every camera prim carries lens data (`focalLength`, `horizontalAperture`, `verticalAperture`, `clippingRange`) and an `Xformable` world transform. `GfFrustum::ComputeCorners()` returns the 8 world-space corners of a frustum given a `GfCamera`.
|
||||
|
||||
`ViewportPanel::Render` already has two FBO overlay call sites (`DrawAxis`, `DrawBoundingBoxes`) and two ImGui DrawList overlay call sites (gizmo, selection rect). The camera wireframe follows the FBO overlay pattern.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Frustum wireframe drawn in 3D world space inside the FBO for every `UsdGeomCamera` prim present in the current stage at time `UsdTimeCode::Default()`.
|
||||
- Three visual states: inactive (muted grey), active/viewport-driving (cyan), selected (yellow/orange matching selection accent).
|
||||
- Click in the viewport selects a camera prim when the mouse is within a pick threshold of a projected frustum segment (tested before the geometry pick).
|
||||
- Selected camera integrates with the existing `TransformManipulator` (translate/rotate).
|
||||
|
||||
**Non-Goals:**
|
||||
- No near/far clip plane rectangle beyond a fixed display depth (far clip could be millions of units; showing a visualisation cap is acceptable).
|
||||
- No billboard label/text annotation.
|
||||
- No custom manipulator handles specific to camera FOV adjustment.
|
||||
- No frustum change when dragging the manipulator (camera shape updates next frame via normal USD dirty propagation).
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — GL overlay into FBO, not ImGui DrawList
|
||||
|
||||
**Decision**: Draw camera wireframes using the existing GLSL line program and `BindDrawTarget`/`UnbindDrawTarget`, the same as `DrawAxis` and `DrawBoundingBoxes`.
|
||||
|
||||
**Rationale**: 3D GL lines drawn into the FBO participate in the depth buffer and are occluded by geometry correctly. ImGui screen-space lines would always appear on top of everything. The existing infrastructure (VAO, VBO, GLSL program, FBO helpers) already supports this pattern at zero additional complexity.
|
||||
|
||||
---
|
||||
|
||||
### D2 — Frustum geometry: near-plane pyramid + body box
|
||||
|
||||
**Decision**: The camera wireframe consists of two parts:
|
||||
1. **Frustum pyramid**: the camera apex (position) connected by 4 lines to the 4 corners of a display near-plane quad. The near clip value from the `UsdGeomCamera` is used but clamped to a minimum of `0.01` to keep it visible.
|
||||
2. **Body box**: a small fixed-size world-space box (side ≈ `frustumScale * 0.15`) centred at the camera position, representing the camera body. This gives a tangible handle at the camera origin that is easy to click.
|
||||
3. **Up arrow**: a short line from the body box top face indicating the camera's up direction, matching the camera local Y axis.
|
||||
|
||||
The far frustum rectangle is **not drawn** — far clip distances are often enormous and would produce confusing off-screen lines.
|
||||
|
||||
**Rationale**: Matches Houdini/Blender's standard camera shape. The body box ensures a reliable click target even at distance.
|
||||
|
||||
---
|
||||
|
||||
### D3 — Frustum scale proportional to camera distance
|
||||
|
||||
**Decision**: The overall wireframe scale is `dist * 0.12` where `dist` is the current viewport camera's look-at distance (from `ViewportCamera::GetDist()`). This matches the `screenFactor` approach used by the `TransformManipulator`.
|
||||
|
||||
**Rationale**: A camera at distance 100 should not appear tiny compared to one at distance 5. Constant apparent size improves usability.
|
||||
|
||||
---
|
||||
|
||||
### D4 — Camera picking via screen-space segment proximity
|
||||
|
||||
**Decision**: `PickCameraAtPoint` iterates all `UsdGeomCamera` prims, projects each wireframe segment to screen space, and returns the prim path of the closest camera whose minimum segment distance to the mouse is `< kCameraPickRadius` (10 pixels). If multiple cameras qualify, the nearest in screen space wins.
|
||||
|
||||
**Rationale**: `UsdImagingGLEngine::TestIntersection` only hits rendered hydra geometry — cameras have no rendered prims in Storm. Custom 2D proximity testing is the only option. 10 px matches the manipulator's pick radius for consistency.
|
||||
|
||||
---
|
||||
|
||||
### D5 — Camera pick tested before geometry pick
|
||||
|
||||
**Decision**: In `ViewportPanel::HandleInput`, the single-click path calls `PickCameraAtPoint` first. If a camera path is returned, that prim is selected and the geometry pick is skipped. If no camera is hit, the existing `PickObject` path runs as before.
|
||||
|
||||
**Rationale**: Camera wireframes are thin and can overlap rendered geometry behind them. Giving camera wireframes priority matches Houdini/Maya behaviour where camera icons are always selectable.
|
||||
|
||||
---
|
||||
|
||||
### D6 — Active camera path passed through from ViewportPanel
|
||||
|
||||
**Decision**: `DrawCameraWireframes` and `PickCameraAtPoint` receive `m_selectedCameraPath` from `ViewportPanel` (the path of whichever USD camera is currently active in the toolbar, or `SdfPath()` for Free Camera). This allows the cyan highlight without coupling `UsdSceneRenderer` to `ViewportCamera`.
|
||||
|
||||
**Rationale**: `UsdSceneRenderer` should not hold a reference to `ViewportCamera`. The path is a cheap `SdfPath` copy per frame.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] Frustum scale is view-dependent** — when the viewport camera is inside a USD camera's frustum and zoomed in close, the scale might get very large.
|
||||
→ Mitigation: clamp `frustumScale` to a maximum of 50 world units.
|
||||
|
||||
- **[Risk] Performance** — iterating all `UsdGeomCamera` prims every frame could be slow for large scenes.
|
||||
→ Mitigation: cache the list of camera prim paths; invalidate on stage change via `SetForceRefresh`.
|
||||
|
||||
- **[Risk] Camera behind the viewer** — `WorldToScreen` returns false for behind-near-plane points; those line endpoints are simply skipped.
|
||||
→ Acceptable; only the visible portion of the wireframe is drawn.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Additive changes only — no existing API is removed or broken. `DrawCameraWireframes` and `PickCameraAtPoint` are new methods; their call sites in `ViewportPanel` are inside existing per-frame code blocks.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## Why
|
||||
|
||||
Camera prims exist in the USD scene but are invisible in the viewport — there is no visual indication of a camera's position, orientation, or field of view. Users cannot tell where cameras are in the scene, cannot click to select them, and cannot use the transform manipulator to reposition them. This is a critical gap for any scene with multiple cameras (rendering, animation, layout).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Draw a 3D frustum wireframe for every `UsdGeomCamera` prim in the viewport, rendered into the FBO using the existing GL overlay infrastructure (`BindDrawTarget`/`UnbindDrawTarget`).
|
||||
- Highlight the **active camera** (the one currently driving the viewport via the camera toolbar) with a distinct colour; all other cameras use the standard camera wire colour.
|
||||
- Highlight the **selected camera** with the selection accent colour.
|
||||
- Support **viewport click-selection** of camera prims by screen-space hit-testing mouse clicks against projected frustum segments.
|
||||
- Camera prims, once selected, can be translated/rotated/scaled through the existing `TransformManipulator` (they are `Xformable`; no manipulator changes are required).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `camera-frustum-overlay`: Per-frame GL wireframe rendering of all `UsdGeomCamera` prims' frustums into the viewport FBO, with colour-coding for active vs. selected vs. inactive cameras.
|
||||
- `camera-viewport-picking`: Screen-space hit-testing that maps a viewport mouse click to a camera prim when the click is within a threshold distance of any projected frustum edge; integrates with the existing prim-selection callbacks.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_(none — no existing spec-level requirements are changing)_
|
||||
|
||||
## Impact
|
||||
|
||||
- **Modified**: `src/core/UsdSceneRenderer.h/.cpp` — add `DrawCameraWireframes(stage, selectedPaths, activeCameraPath, viewProjMatrix)` and `PickCameraAtPoint(stage, mouseScreenPos, viewProjMatrix, imagePos, viewW, viewH, outPath)`.
|
||||
- **Modified**: `src/ui/ViewportPanel.cpp` — call `DrawCameraWireframes` each frame after `DrawBoundingBoxes`; call `PickCameraAtPoint` inside the single-click pick path before the existing `PickObject` geometry pick so camera hits take priority.
|
||||
- No new external dependencies. Uses existing GLSL shader program already compiled for `DrawAxis`/`DrawBoundingBoxes`.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera frustum wireframe drawn every frame
|
||||
For every `UsdGeomCamera` prim present in the current USD stage, the system SHALL draw a 3D wireframe representation into the viewport FBO each frame. The wireframe SHALL be drawn at `UsdTimeCode::Default()`. The wireframe SHALL consist of: (a) a small body box centred at the camera origin, (b) four lines from the camera origin to the corners of a near-clip display quad, and (c) a short up-arrow line indicating the camera's local Y axis.
|
||||
|
||||
#### Scenario: Camera wireframe appears when camera prim exists
|
||||
- **WHEN** a `UsdGeomCamera` prim exists in the stage
|
||||
- **THEN** a frustum wireframe is visible in the viewport at the camera prim's world-space position and orientation
|
||||
|
||||
#### Scenario: No wireframe when stage has no cameras
|
||||
- **WHEN** no `UsdGeomCamera` prims exist in the stage
|
||||
- **THEN** no camera wireframe is drawn
|
||||
|
||||
### Requirement: Wireframe size scales with viewport camera distance
|
||||
The wireframe scale SHALL be proportional to the current viewport camera's look-at distance (`ViewportCamera::GetDist()`), clamped to a maximum of 50 world units, so that camera shapes maintain a consistent apparent screen size regardless of zoom level.
|
||||
|
||||
#### Scenario: Camera wireframe stays visible when zooming out
|
||||
- **WHEN** the viewport camera is zoomed out (increasing distance)
|
||||
- **THEN** the camera wireframe grows proportionally so it remains visible at roughly the same screen size
|
||||
|
||||
### Requirement: Three distinct visual states
|
||||
The system SHALL draw camera wireframes using three colour states:
|
||||
- **Inactive** (default): muted grey `(0.55, 0.55, 0.55, 0.85)`
|
||||
- **Active** (the camera currently driving the viewport via the camera toolbar): cyan `(0.2, 0.9, 1.0, 1.0)`
|
||||
- **Selected** (the camera prim is the current primary selection): yellow-orange `(1.0, 0.75, 0.1, 1.0)`
|
||||
|
||||
A camera that is both active and selected SHALL use the selected colour (selected takes priority).
|
||||
|
||||
#### Scenario: Active camera shown in cyan
|
||||
- **WHEN** a USD camera prim is set as the active viewport camera via the camera toolbar
|
||||
- **THEN** its wireframe is drawn in cyan
|
||||
|
||||
#### Scenario: Selected camera shown in yellow-orange
|
||||
- **WHEN** a camera prim is the primary selected prim in the scene hierarchy
|
||||
- **THEN** its wireframe is drawn in yellow-orange, overriding the cyan active colour
|
||||
|
||||
### Requirement: Camera list cached and refreshed on stage change
|
||||
The system SHALL cache the list of `UsdGeomCamera` prim paths to avoid re-traversing the stage every frame. The cache SHALL be invalidated whenever `SetForceRefresh(true)` is called or the stage pointer changes.
|
||||
|
||||
#### Scenario: Cache invalidated on stage reload
|
||||
- **WHEN** a new USD file is opened
|
||||
- **THEN** the camera wireframe list reflects the new stage's cameras in the next frame
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera prim selectable by clicking its wireframe
|
||||
The system SHALL test each viewport left-mouse-button click against all camera wireframe segments before running the existing geometry pick. If the click position is within 10 screen pixels of any projected wireframe edge, the corresponding `UsdGeomCamera` prim SHALL be selected. Among multiple qualifying cameras the nearest one (smallest minimum screen distance) SHALL be chosen.
|
||||
|
||||
#### Scenario: Clicking camera wireframe selects the camera prim
|
||||
- **WHEN** the user left-clicks within 10 pixels of a camera wireframe segment
|
||||
- **THEN** the camera prim appears selected in the Scene Hierarchy panel and the Property panel shows its attributes
|
||||
|
||||
#### Scenario: Camera pick takes priority over geometry behind it
|
||||
- **WHEN** the user clicks on a camera wireframe that visually overlaps rendered geometry
|
||||
- **THEN** the camera prim is selected, not the geometry behind it
|
||||
|
||||
#### Scenario: Click misses all cameras — falls through to geometry pick
|
||||
- **WHEN** the user left-clicks on an area with no camera wireframe within 10 pixels
|
||||
- **THEN** the normal geometry pick runs as before
|
||||
|
||||
### Requirement: Selected camera is editable with the transform manipulator
|
||||
After a camera prim is selected via viewport click, the system SHALL allow the `TransformManipulator` to translate and rotate it using the existing W/E/R tool modes. The camera SHALL implement `UsdGeomXformCommonAPI` for the manipulator to write to. No additional manipulator changes are required.
|
||||
|
||||
#### Scenario: Move manipulator repositions selected camera
|
||||
- **WHEN** a camera prim is selected and the Move tool (W) is active
|
||||
- **THEN** the translate gizmo appears at the camera's world position and dragging it moves the camera prim
|
||||
|
||||
#### Scenario: Rotate manipulator reorients selected camera
|
||||
- **WHEN** a camera prim is selected and the Rotate tool (E) is active
|
||||
- **THEN** the rotate rings appear at the camera's world position and dragging them rotates the camera prim
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
## 1. UsdSceneRenderer — DrawCameraWireframes
|
||||
|
||||
- [ ] 1.1 Add to `UsdSceneRenderer.h`: `void DrawCameraWireframes(pxr::UsdStageRefPtr stage, const pxr::SdfPathVector& selectedPaths, const pxr::SdfPath& activeCameraPath, const pxr::GfMatrix4d& viewProjMatrix, double viewportCameraDist)` and private helpers `BuildCameraWireframeLines(const pxr::GfCamera& gfCam, double scale, std::vector<float>& outVerts)` and `pxr::SdfPathVector m_cachedCameraPaths` + `bool m_cameraCacheDirty = true`
|
||||
- [ ] 1.2 In `UsdSceneRenderer.cpp` implement `DrawCameraWireframes`:
|
||||
- If `m_cameraCacheDirty`, traverse stage for `UsdGeomCamera` prims and cache paths; clear dirty flag
|
||||
- For each cached camera path, resolve world transform + lens params via `UsdGeomCamera::GetCamera(UsdTimeCode::Default())` → `GfCamera`
|
||||
- Compute `frustumScale = std::min(viewportCameraDist * 0.12, 50.0)`
|
||||
- Call `BuildCameraWireframeLines` to produce a `std::vector<float>` of XYZ vertex pairs (GL_LINES)
|
||||
- Determine colour: selected → `(1.0,0.75,0.1,1.0)`, active → `(0.2,0.9,1.0,1.0)`, inactive → `(0.55,0.55,0.55,0.85)`
|
||||
- `BindDrawTarget()`, draw all segments using the existing axis GLSL program + a dynamic VBO (same pattern as `DrawBoundingBoxes`), `UnbindDrawTarget()`
|
||||
- [ ] 1.3 Implement `BuildCameraWireframeLines`: given `GfCamera` and scale, produce:
|
||||
- **Body box** — 12 edges of a small box (width × height × depth = scale×0.15 each) centred at camera origin in local space, transformed to world space by the camera's transform matrix
|
||||
- **Frustum pyramid** — 4 lines from camera origin to each corner of a near-display quad at depth `max(nearClip, 0.01)`, scaled so the quad width/height match `horizontalAperture/focalLength * nearDepth` (perspective divide)
|
||||
- **Up arrow** — one line from body-box top-centre upward by `scale * 0.2` along camera local Y
|
||||
- [ ] 1.4 Set `m_cameraCacheDirty = true` inside `SetStage` (already exists) and inside the `SetForceRefresh(true)` path
|
||||
|
||||
## 2. UsdSceneRenderer — PickCameraAtPoint
|
||||
|
||||
- [ ] 2.1 Add to `UsdSceneRenderer.h`: `bool PickCameraAtPoint(pxr::UsdStageRefPtr stage, const ImVec2& mousePosAbsolute, const pxr::GfMatrix4d& viewProjMatrix, const ImVec2& imagePos, int viewW, int viewH, double viewportCameraDist, pxr::SdfPath* outCameraPath)`
|
||||
- [ ] 2.2 In `UsdSceneRenderer.cpp` implement `PickCameraAtPoint`:
|
||||
- Iterate `m_cachedCameraPaths` (build cache if needed)
|
||||
- For each camera: build wireframe vertex list via `BuildCameraWireframeLines`
|
||||
- Project each vertex pair to screen space using the same `WorldToScreen` math as `TransformManipulator`
|
||||
- Compute `PointToSegmentDist` for each segment against `mousePosAbsolute`
|
||||
- Track minimum distance and corresponding path; if `< kCameraPickRadius (10 px)` return true with that path
|
||||
- If tie between multiple cameras, return the one with the smallest minimum distance
|
||||
|
||||
## 3. ViewportPanel — Integration
|
||||
|
||||
- [ ] 3.1 In `ViewportPanel.h`: add `pxr::SdfPath GetActiveCameraPath() const` (returns `m_camera.GetUsdCameraPath()` when in UsdCamera mode, else `SdfPath()`) — check `ViewportCamera` API for the getter name
|
||||
- [ ] 3.2 In `ViewportPanel.cpp` `Render()`: after the `DrawBoundingBoxes` call, add:
|
||||
```cpp
|
||||
m_renderer.DrawCameraWireframes(m_stage, m_selectedSdfPaths,
|
||||
m_camera.GetUsdCameraPath(), viewProj, m_camera.GetDist());
|
||||
```
|
||||
(Use the local `viewProj` already computed in that scope)
|
||||
- [ ] 3.3 In `ViewportPanel.cpp` `HandleInput()`, single-click path (before the existing `PickObject` call): add camera pick:
|
||||
```cpp
|
||||
pxr::SdfPath camPath;
|
||||
if (m_renderer.PickCameraAtPoint(m_stage, mouse, viewProj,
|
||||
m_imageScreenPos, m_viewWidth, m_viewHeight,
|
||||
m_camera.GetDist(), &camPath)) {
|
||||
// fire selection callbacks with camPath
|
||||
}
|
||||
```
|
||||
Only fall through to `PickObject` if `PickCameraAtPoint` returns false
|
||||
|
||||
## 4. Build and Verification
|
||||
|
||||
- [ ] 4.1 Run `cmake --preset default` (only needed if new `.cpp` files were added; all changes here are in existing files so this may be skipped) then `cmake --build build --config Release` — resolve any compilation errors
|
||||
- [ ] 4.2 Install and launch `install/bin/App.exe`; create a `Camera` prim via Stage menu, verify its frustum wireframe appears in the viewport at the world origin
|
||||
- [ ] 4.3 Verify clicking the camera wireframe selects the prim in the Scene Hierarchy panel
|
||||
- [ ] 4.4 Verify the Move (W) gizmo appears on the selected camera and dragging repositions it; the wireframe moves to match
|
||||
- [ ] 4.5 Switch the viewport to that camera via the camera toolbar; verify the wireframe turns cyan
|
||||
- [ ] 4.6 With the camera active in the toolbar, click it in the viewport; verify it turns yellow-orange (selected overrides active colour)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## Context
|
||||
|
||||
The USD Layer Manager application uses ImGui for its user interface. Currently, ImGui uses its default font, which is functional but not optimized for modern UI aesthetics. The proposal outlines changing to the Inter font to improve readability and visual appeal.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Load and apply the Inter font in ImGui initialization
|
||||
- Ensure consistent font usage across the application UI
|
||||
- Minimal performance impact on startup
|
||||
|
||||
**Non-Goals:**
|
||||
- Implement font selection options for users
|
||||
- Support multiple font weights or styles beyond regular
|
||||
- Modify font rendering beyond basic loading
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Font Choice**: Use Inter Regular (Inter-Regular.ttf) as it provides excellent readability for UI text. Considered alternatives like Roboto, but Inter is specifically designed for interfaces and is more modern.
|
||||
- **Loading Location**: Load the font in the main application initialization, likely in src/main.cpp where ImGui context is set up, to ensure it's available before any UI rendering.
|
||||
- **Font Size**: Maintain default ImGui font size (13pt) to avoid layout changes.
|
||||
- **Fallback**: If Inter font fails to load, fall back to default font to prevent application failure.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Font File Dependency**: Requires Inter-Regular.ttf to be included in project assets. Risk of missing file → Mitigation: Add to version control and build process checks.
|
||||
- **Startup Performance**: Loading a custom font may add slight delay to initialization. Risk: Negligible impact expected, as font loading is typically fast.
|
||||
- **Cross-Platform Compatibility**: Font rendering may vary slightly on different platforms. Risk: Test on target platforms (Windows primarily).
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## Why
|
||||
|
||||
The current ImGui interface uses the default font, which may not provide the best readability and modern aesthetics. Changing to Inter, a popular and highly readable typeface designed for user interfaces, will improve the overall user experience and visual appeal of the USD Layer Manager application.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Update ImGui font initialization to load and use the Inter font face instead of the default font.
|
||||
- Add Inter font file to the project resources.
|
||||
- Ensure the font is properly loaded at application startup.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `inter-font-setup`: Configure ImGui to use the Inter font for improved UI readability and aesthetics.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- No existing capabilities are being modified -->
|
||||
|
||||
## Impact
|
||||
|
||||
- UI rendering system will use Inter font, affecting all text display in the application.
|
||||
- Requires adding Inter font file to project assets.
|
||||
- No breaking changes to APIs or existing functionality.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Load Inter Font
|
||||
The system SHALL load the Inter-Regular.ttf font file from the project assets during application initialization.
|
||||
|
||||
#### Scenario: Font file exists
|
||||
- **WHEN** the application starts up and Inter-Regular.ttf is present in the assets
|
||||
- **THEN** the font SHALL be loaded successfully into ImGui
|
||||
|
||||
#### Scenario: Font file missing
|
||||
- **WHEN** the application starts up and Inter-Regular.ttf is not found
|
||||
- **THEN** the system SHALL fall back to the default ImGui font and log a warning
|
||||
|
||||
### Requirement: Apply Inter Font as Default
|
||||
The system SHALL set the loaded Inter font as the default font for all ImGui text rendering.
|
||||
|
||||
#### Scenario: Font loaded successfully
|
||||
- **WHEN** the Inter font is loaded successfully
|
||||
- **THEN** all UI text SHALL render using the Inter font face
|
||||
|
||||
#### Scenario: Font loading fails
|
||||
- **WHEN** the Inter font fails to load
|
||||
- **THEN** the system SHALL continue with the default ImGui font
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
## 1. Asset Preparation
|
||||
|
||||
- [x] 1.1 Download Inter-Regular.ttf and add to project assets directory
|
||||
|
||||
## 2. Code Implementation
|
||||
|
||||
- [x] 2.1 Modify src/main.cpp to load Inter font during ImGui initialization
|
||||
- [x] 2.2 Implement fallback to default font if Inter loading fails
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [x] 3.1 Build the application and verify Inter font is displayed
|
||||
- [x] 3.2 Test font loading error handling by temporarily removing font file
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
## Context
|
||||
|
||||
The application currently has a `SceneHierarchyPanel` class (92 lines) that implements a functional tree view of USD prims:
|
||||
- Uses `PropertyManager::GetPrimPaths()` to get all prims
|
||||
- Recursively renders prims with `RenderPrimNode()` showing name, type, and selection state
|
||||
- Supports selection highlighting, tooltips with type/path, and double-click/arrow expansion
|
||||
- Has a callback mechanism (`PrimSelectCallback`) for notifying when a prim is selected
|
||||
|
||||
However, `Application::RenderUI()` (lines 94-103) ignores this class and renders an inline placeholder instead:
|
||||
```cpp
|
||||
ImGui::Begin("Scene Hierarchy", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
if (m_stageManager->HasStage()) {
|
||||
ImGui::Text("Stage loaded: %s", m_stageManager->GetRootLayerIdentifier().c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Scene hierarchy will be displayed here");
|
||||
} else {
|
||||
ImGui::TextDisabled("No stage loaded");
|
||||
ImGui::Text("Open a USD file to view scene hierarchy");
|
||||
}
|
||||
ImGui::End();
|
||||
```
|
||||
|
||||
Meanwhile, the `LayerPanel` class is properly wired in (lines 105-111) and functions correctly:
|
||||
```cpp
|
||||
ImGui::Begin("Layer Panel", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
if (m_stageManager->HasStage()) {
|
||||
m_layerPanel->Render();
|
||||
} else {
|
||||
ImGui::TextDisabled("No stage loaded");
|
||||
}
|
||||
ImGui::End();
|
||||
```
|
||||
|
||||
The `PropertyPanel` class exists as a header only (no .cpp file) and is also rendered as a placeholder.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Wire the existing `SceneHierarchyPanel` into `Application` so it functions as a dockable panel
|
||||
- Enhance the panel with prim type icons, visibility/active state indicators, and context menus
|
||||
- Ensure prim selection drives other panels (Property Panel) via the existing callback mechanism
|
||||
- Maintain consistency with the existing `LayerPanel` wiring pattern
|
||||
|
||||
**Non-Goals:**
|
||||
- Do not create a new panel class from scratch (the class already exists)
|
||||
- Do not modify the core USD traversal logic in `PropertyManager` (it's already functional)
|
||||
- Do not implement 3D viewport or property editing in this change
|
||||
- Do not change the docking architecture or ImGui context management
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Wiring Pattern: Follow Existing LayerPanel Approach
|
||||
**Decision:** Wire `SceneHierarchyPanel` exactly like `LayerPanel` - create instance in `Application::Initialize()`, call `SetPropertyManager()`, and invoke `Render()` in `Application::RenderUI()`.
|
||||
|
||||
**Rationale:**
|
||||
- Consistency with existing working pattern (`LayerPanel`)
|
||||
- Minimal changes required
|
||||
- Clear separation of concerns (Application manages lifecycle, panel handles rendering)
|
||||
- Avoids duplicating the placeholder code
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Inline rendering in Application (current placeholder) - rejected because it duplicates existing functionality
|
||||
- Creating panel on-demand in RenderUI - rejected because it loses state and breaks consistency
|
||||
|
||||
### 2. Enhancement: Add Visual Indicators for Prim State
|
||||
**Decision:** Enhance `SceneHierarchyPanel::RenderPrimNode()` to show:
|
||||
- Prim type icons (using Unicode characters or colored text)
|
||||
- Visibility/inactive state indicators (eye icon or strike-through)
|
||||
- Reference/instance indicators
|
||||
- Context menu for common operations (toggle visibility, toggle active)
|
||||
|
||||
**Rationale:**
|
||||
- Provides immediate visual feedback about prim state without opening property panel
|
||||
- Matches user expectations from similar tools (Maya Outliner, Unity Hierarchy)
|
||||
- Leverages existing USD API (`GetVisibility()`, `GetActive()`)
|
||||
- Non-invasive enhancement that builds on solid foundation
|
||||
|
||||
**Implementation Notes:**
|
||||
- Use `prim.GetVisibility()` and `prim.GetActive()` to determine state
|
||||
- Show icons before prim name: 👁️ (visible), 👁️🗨️ (invisible), ○ (active), ● (inactive)
|
||||
- Context menu via `ImGui::BeginPopupContextItem()` on the tree node
|
||||
|
||||
### 3. Integration: Connect Selection to Property Panel
|
||||
**Decision:** Implement the `PrimSelectCallback` in `Application` to update the `PropertyPanel` when selection changes (once PropertyPanel is implemented).
|
||||
|
||||
**Rationale:**
|
||||
- Leverages existing callback mechanism in SceneHierarchyPanel
|
||||
- Enables future integration with PropertyPanel
|
||||
- Follows event-driven pattern already established
|
||||
- No changes needed to SceneHierarchyPanel itself
|
||||
|
||||
**Note:** PropertyPanel currently doesn't exist as .cpp, so this sets up the foundation for when it's implemented.
|
||||
|
||||
### 4. Performance: Maintain Existing Traversal Approach
|
||||
**Decision:** Keep the existing `PropertyManager::GetPrimPaths()` + recursive traversal approach.
|
||||
|
||||
**Rationale:**
|
||||
- Already implemented and working
|
||||
- `GetPrimPaths()` uses efficient `UsdPrimRange` traversal
|
||||
- Caching isn't needed for typical scene sizes (<10k prims)
|
||||
- Premature optimization would complicate the clean implementation
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
[Performance] → Acceptable for typical USD scenes; `GetPrimPaths()` is O(N) but only called when stage changes or panel refreshes needed.
|
||||
|
||||
[UI Clutter] → Added icons and context menus enhance usability without overwhelming the interface; users can ignore extra visual cues if not needed.
|
||||
|
||||
[Tight Coupling] → Application now depends on SceneHierarchyPanel class; however, this follows the same pattern as LayerPanel and is acceptable for core UI components.
|
||||
|
||||
[Missing PropertyPanel] → Selection callback won't have immediate effect until PropertyPanel is implemented; this is expected and sets up proper integration for future work.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None - the path forward is clear: wire the existing panel, enhance it with visual indicators, and maintain consistency with existing patterns.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
## Why
|
||||
|
||||
The application currently has a `SceneHierarchyPanel` class that renders a prim tree using USD traversal, but `Application::RenderUI()` ignores it and renders an inline placeholder instead. Users cannot browse the USD prim hierarchy in the Scene Hierarchy panel — the foundational feature for this USD editing tool.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Wire the existing `SceneHierarchyPanel` class into `Application` so it renders in the "Scene Hierarchy" dockable window
|
||||
- Enhance the panel to display prim type icons, visibility/active state indicators, and structured prim path hierarchy
|
||||
- Support prim selection that can drive other panels (Property Panel, Layer Panel) via the existing callback mechanism
|
||||
- Add context menu support for common prim operations (toggle visibility, toggle active, show/hide children)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `scene-hierarchy-panel`: Renders a tree view of USD prims from the loaded stage, with selection, type indicators, and prim state toggles. Wired as a dockable panel in the application layout.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing specs have requirement-level changes -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected code**: `src/ui/Application.h/.cpp` (wiring), `src/ui/SceneHierarchyPanel.h/.cpp` (enhancements)
|
||||
- **Dependencies**: `UsdStageManager` (stage access), `PropertyManager` (prim traversal), `LayerManager` (layer context)
|
||||
- **No breaking changes** — the existing placeholder is replaced with the functional panel
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Scene hierarchy panel displays USD prim tree
|
||||
The system SHALL display a hierarchical tree view of all prims in the currently loaded USD stage, allowing users to browse and select prims.
|
||||
|
||||
#### Scenario: Panel shows prim hierarchy when stage loaded
|
||||
- **WHEN** a USD stage is successfully loaded and the Scene Hierarchy panel is visible
|
||||
- **THEN** the panel displays a tree view containing all prims from the stage, organized by parent-child relationships
|
||||
- **AND** each prim node displays its name, type, and visual indicators for visibility/active state
|
||||
- **AND** the root prims (children of the pseudo-root) are displayed at the top level
|
||||
|
||||
#### Scenario: Panel shows empty state when no stage loaded
|
||||
- **WHEN** no USD stage is loaded (or stage creation/opening failed) and the Scene Hierarchy panel is visible
|
||||
- **THEN** the panel displays disabled text indicating no stage is loaded
|
||||
- **AND** suggests opening a USD file to view scene hierarchy
|
||||
|
||||
#### Scenario: Prim selection highlights in tree
|
||||
- **WHEN** a prim is selected in the scene hierarchy tree (via click)
|
||||
- **THEN** that prim's node is highlighted as selected in the tree view
|
||||
- **AND** the panel's selected prim path is updated to match the clicked prim's path
|
||||
- **AND** the prim selection callback is invoked with the selected prim's path
|
||||
|
||||
#### Scenario: Prim type visible in tooltip
|
||||
- **WHEN** the user hovers over a prim node in the scene hierarchy tree
|
||||
- **THEN** a tooltip appears showing the prim's type name and full path
|
||||
- **AND** the tooltip disappears when the cursor leaves the prim node
|
||||
|
||||
#### Scenario: Context menu available on prim nodes
|
||||
- **WHEN** the user right-clicks on a prim node in the scene hierarchy tree
|
||||
- **THEN** a context menu appears with options for common prim operations
|
||||
- **AND** the context menu includes options to toggle visibility and toggle active state
|
||||
- **AND** selecting a context menu option applies the corresponding operation to the prim
|
||||
|
||||
#### Scenario: Panel integrates with application docking system
|
||||
- **WHEN** the Scene Hierarchy panel is created as part of the application UI
|
||||
- **THEN** the panel appears as a dockable window titled "Scene Hierarchy"
|
||||
- **AND** the panel can be undocked, floated, resized, and docked to other positions in the UI
|
||||
- **AND** the panel respects the NoCollapse window flag (cannot be collapsed to title bar only)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## 1. Wire SceneHierarchyPanel into Application
|
||||
|
||||
- [x] 1.1 Add SceneHierarchyPanel member variable to Application class
|
||||
- [x] 1.2 Create SceneHierarchyPanel instance in Application::Initialize()
|
||||
- [x] 1.3 Set PropertyManager on SceneHierarchyPanel instance
|
||||
- [x] 1.4 Replace placeholder Scene Hierarchy panel code with call to m_sceneHierarchyPanel->Render()
|
||||
- [x] 1.5 Ensure SceneHierarchyPanel is properly cleaned up in Application::Shutdown()
|
||||
|
||||
## 2. Enhance SceneHierarchyPanel with Visual Indicators
|
||||
|
||||
- [x] 2.1 Modify RenderPrimNode to display prim type icons before the display name
|
||||
- [x] 2.2 Add visibility state indicators (eye/open vs eye/slash) based on prim.GetVisibility()
|
||||
- [x] 2.3 Add active state indicators (circle/filled circle) based on prim.GetActive()
|
||||
- [x] 2.4 Ensure proper indentation and spacing with the added indicators
|
||||
- [x] 2.5 Update tooltip to include visibility and active state information
|
||||
|
||||
## 3. Add Context Menu Support
|
||||
|
||||
- [x] 3.1 Add context menu popup on right-click of prim tree nodes
|
||||
- [x] 3.2 Add "Toggle Visibility" menu item that calls prim.SetVisibility() with toggled value
|
||||
- [x] 3.3 Add "Toggle Active" menu item that calls prim.SetActive() with toggled value
|
||||
- [x] 3.4 Add "Show/Hide Children" menu item to collapse/expand tree nodes
|
||||
- [x] 3.5 Ensure context menu only appears for valid prim nodes and handles USD errors gracefully
|
||||
|
||||
## 4. Verify Selection and Integration
|
||||
|
||||
- [x] 4.1 Verify prim selection highlighting works correctly in enhanced tree view
|
||||
- [x] 4.2 Confirm tooltip shows all relevant information (type, path, visibility, active)
|
||||
- [x] 4.3 Test that selection callback is properly invoked when prim is clicked
|
||||
- [x] 4.4 Ensure panel updates correctly when stage changes (new/loaded/closed)
|
||||
- [x] 4.5 Verify panel integrates properly with docking system (can be undocked, floated, etc.)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-30
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
## Context
|
||||
|
||||
The app currently has a single `ViewportPanel` class that owns one `ViewportCamera`, one `UsdSceneRenderer`, and draws a full-window render with toolbars + overlays. Selection propagates through `OnPrimPicked` / `OnPrimsPickedRect` callbacks to `SceneHierarchyPanel` and `PropertyPanel`. The `UsdSceneRenderer` owns a `GlfDrawTarget` FBO; the render loop calls `m_renderer.Render()` then renders overlays (axis, bbox, gizmo, manipulator toolbar) into the same FBO or directly to the screen via ImGui draw lists.
|
||||
|
||||
The `ViewportCamera` and `UsdSceneRenderer` classes are already designed as independent units — they can be instantiated per-viewport without modification. The key architectural challenge is sharing the selection state across N viewport tiles while ensuring only the focused tile renders the transform gizmo.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Support 1, 2 (horizontal/vertical split), and 4-quadrant viewport layouts
|
||||
- Each viewport tile has its own camera, renderer, and per-viewport settings (grid, AA, bbox mode, background color, render delegate)
|
||||
- Single shared selection: picking in any tile updates all tiles and propagates to panels
|
||||
- Transform manipulator (gizmo) renders only in the focused tile
|
||||
- Drag-based dividers to resize tiles within a layout
|
||||
- Backward-compatible `Application` API: `m_viewportPanel` stays as the entry point
|
||||
- Space-key maximize/restore: pressing Space over the hovered tile temporarily fills the entire viewport area; pressing Space again restores the previous layout (Maya-style)
|
||||
|
||||
**Non-Goals:**
|
||||
- Not adding tear-off/undockable viewport windows (ImGui docking handles that separately)
|
||||
- Not adding per-viewport layer visibility overrides (future feature)
|
||||
- Not supporting more than 4 tiles (can be extended later)
|
||||
- Not adding viewport sync (playblasting) or timeline scrubbing across tiles
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Extract `ViewportTile` from `ViewportPanel` rather than subclassing.**
|
||||
- The existing 1100-line `ViewportPanel::Render()` method contains everything: toolbar rendering, camera resolution, scene rendering, overlay drawing, context menu, and manipulator. Extracting a `ViewportTile` class that owns a camera + renderer + local per-viewport settings + the subset of overlay logic that is per-tile keeps the container clean.
|
||||
- `ViewportPanel` becomes the container: it owns `std::vector<std::unique_ptr<ViewportTile>>`, manages layout geometry, and delegates `Render()` calls to each tile.
|
||||
- Alternative considered: having `ViewportPanel` contain N inline structs. This was rejected because tiles need their own camera/resolution/overlay lifecycle, which calls for a proper class.
|
||||
|
||||
2. **Selection lives on the container (`ViewportPanel`), not on any tile.**
|
||||
- Each tile still calls `m_renderer.SetSelectedPaths()` and `m_renderer.AddSelected()` so its `UsdImagingGLEngine` highlights the correct prims, but the authoritative `m_selectedSdfPaths` / `m_selectedPrimPath` vector lives on `ViewportPanel`.
|
||||
- When a tile receives a pick hit, it calls back into the container to update the shared selection, and the container broadcasts it to all tiles and to the `OnPrimPicked`/`OnPrimsPickedRect` callbacks.
|
||||
- Alternative considered: each tile owns its selection and syncs via signals. Rejected because it adds complexity for a single-authority selection model.
|
||||
|
||||
3. **Manipulator ownership moves to `ViewportPanel` (container), not per tile.**
|
||||
- The `TransformManipulator` is a singleton — there should never be two gizmos active. The container owns it and only passes it to the focused tile for rendering.
|
||||
- The focused tile is the one that last received a `ImGui::IsWindowHovered()` click. Tile tracks `m_isFocused` set by container during `HandleInput()`.
|
||||
|
||||
4. **Layout state stored as simple enum + divider positions, no external dependency.**
|
||||
- Layout enum values: `Single`, `HSplit` (2 tiles side-by-side), `VSplit` (2 tiles top-bottom), `Quad` (4 tiles).
|
||||
- Divider positions stored as normalized floats (`0.0–1.0`). Dragging a divider updates the normalized position, then `ViewportPanel::Render()` recomputes tile rectangles.
|
||||
- No XML/JSON config for layout — state is ephemeral (could be persisted via future settings system).
|
||||
- Alternative considered: `ImGui::Splitter()` based approach from ImGui demos. We'll implement a lightweight version directly since we need precise control over tile rectangle subdivision.
|
||||
|
||||
5. **Render delegate can differ per tile.**
|
||||
- Each tile's `UsdSceneRenderer` independently calls `SetRendererPlugin()`. This means each tile FBO can use e.g. Storm vs HdEmbree independently.
|
||||
- This is useful for comparing renderers side-by-side and is already supported by `UsdSceneRenderer`'s per-instance design.
|
||||
|
||||
6. **Space-key maximize stored as a simple toggle on the container, not on individual tiles.**
|
||||
- `ViewportPanel` stores `m_maximizedTileIndex` (-1 = not maximized) and `m_layoutBeforeMaximize` (the `LayoutMode` enum value before Space was pressed).
|
||||
- When Space is pressed (with tile hovered and `m_maximizedTileIndex == -1`): save `m_layoutBeforeMaximize`, set `m_maximizedTileIndex` to the hovered tile, and render only that tile filling the entire viewport area (no dividers, no layout menu).
|
||||
- When Space is pressed again (or Escape): restore `m_maximizedTileIndex` to -1 and re-render the saved layout.
|
||||
- Keyboard shortcuts (F, A, Q/W/E/R) continue to target the maximized tile as the de facto focused tile.
|
||||
- Alternative considered: pushing a separate layout mode `Maximized`. Rejected because it conflates layout state with a transient display mode; storing the pre-maximize layout explicitly is cleaner and avoids state machine complexity.
|
||||
|
||||
7. **Per-tile settings stored on `ViewportTile`, not on a settings struct singleton.**
|
||||
- Grid/AA/bg-color/bbox-mode/render-delegate are all fields on `ViewportTile`. The compact toolbar in each tile reads/writes these directly.
|
||||
- For layout persistence (future), tiles could serialize their settings.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| N renderers means N FBO draw targets, each at full viewport resolution. For a 4K monitor with Quad layout, each tile is ~960×540 → 4× FBO allocations at that size. GPU memory may spike. | Cap layout to 4 tiles. Add a `m_renderScale` per tile (default 1.0) if perf issues arise. OpenUSD's `UsdImagingGLEngine` already has `renderParams` for resolution scaling. |
|
||||
| Input routing: multiple tiles competing for Alt+LMB, Alt+RMB, scroll, etc. | The container dispatches `HandleInput()` only to the **hovered** tile each frame. The focused tile's manipulator is the only one that can consume input. |
|
||||
| Right-click context menu: which tile owns it? | The context menu appears on the hovered tile. The popup (`ViewportContextMenuN`) is opened per-tile using unique IDs. |
|
||||
| Divider drag area is thin — hard to click on high-DPI. | Dividers are 6px wide with a 2px visual line in the centre. Cursor changes to resize cursor on hover. |
|
||||
| Keyboard shortcuts (F=frame, A=frame all, Q/W/E/R=manipulator) need to target the focused tile. | Shortcuts in the container delegate to the focused tile's camera/manipulator. If no tile is focused (e.g. user just clicked a panel outside the viewport), shortcuts are ignored. |
|
||||
| Space key might conflict with ImGui docking or text input. | Guard with `!io.WantTextInput` (same as existing F/A shortcuts). Space is not a default ImGui docking shortcut, so no conflict with docking. |
|
||||
| User might accidentally press Space when no tile is hovered (e.g. focus is on a panel). | Space is a no-op when no tile is hovered and viewport is not maximized. If already maximized, Space always restores regardless of hover state. |
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
## Why
|
||||
|
||||
Multi-viewport support is a standard feature in DCC tools (Maya, Houdini, Blender) that lets users view the same scene from different camera angles simultaneously. Currently the app has a single `ViewportPanel` — users cannot compare views (e.g., free camera + a USD camera prim, or top/orthographic + perspective) without switching back and forth. This limits scene inspection, layout work, and side-by-side comparison during shading and lighting iteration.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a **viewport layout system** that manages 1–4 viewport tiles within a single ImGui window area
|
||||
- Each tile has its own `ViewportCamera`, `UsdSceneRenderer`, selection highlight set, and per-viewport settings (grid, AA, background color, bounding box mode)
|
||||
- Selection is **shared** across all viewports (pick in any viewport, all viewports highlight)
|
||||
- The main `ViewportPanel` becomes a **container** that holds N child viewports (N=1 default, up to 4)
|
||||
- **Layout presets**: single, 2-split horizontal/vertical, 4-quadrant
|
||||
- Per-viewport camera toolbar remains (camera selector, render delegate, view options) but is **compact** (icon-only) to conserve space
|
||||
- The manipulator gizmo is only active in the **focused** viewport (the one that last received a click)
|
||||
- **Space-key maximize**: pressing Space while hovering over a tile temporarily maximizes that tile to fill the entire viewport area, hiding dividers and other tiles. Pressing Space again restores the previous multi-tile layout (identical to Maya's viewport maximize behavior)
|
||||
- **BREAKING**: `Application`'s `m_viewportPanel` member type changes (it remains `ViewportPanel` but `ViewportPanel` switches from a single renderer to a container)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `viewport-layout`: Layout management — splitting, merging, preset switching, drag-resize dividers, Space-key maximize/restore, persistence of layout choice
|
||||
- `per-viewport-camera`: Each tile has an independent `ViewportCamera`, camera selector dropdown, and free/USD camera switching
|
||||
- `per-viewport-settings`: Per-tile grid toggle, AA, background color, bounding box mode, render delegate selection (independent per viewport)
|
||||
- `shared-selection`: Single selection model across all viewports — picking in any tile updates all others and propagates to SceneHierarchyPanel / PropertyPanel
|
||||
- `focused-viewport-manipulator`: Transform gizmo only renders and accepts input in the most recently clicked viewport tile
|
||||
|
||||
### Modified Capabilities
|
||||
- *(None — `imgui-docking` is purely an ImGui infrastructure change; the new viewport system builds on top of it and does not alter its spec)*
|
||||
|
||||
## Impact
|
||||
|
||||
- **`src/ui/ViewportPanel.h/.cpp`**: Rewritten from a single-viewport class to a container that owns N `ViewportTile` instances. Public API surface changes — `SetSelectedPrimPath`, `SetStage`, `GetCamera`, `GetRenderer` remain but are forwarded to the active/focused tile or broadcast to all tiles.
|
||||
- **`src/core/UsdSceneRenderer.h/.cpp`**: No changes needed (each tile owns its own `UsdSceneRenderer` already).
|
||||
- **`src/core/ViewportCamera.h/.cpp`**: No changes needed (each tile owns its own `ViewportCamera` already).
|
||||
- **`src/ui/Application.h/.cpp`**: Wire up `OnPrimPicked` / `OnPrimsPickedRect` as before on the container; the container relays selection to the `SceneHierarchyPanel` / `PropertyPanel`.
|
||||
- **New files**: `src/ui/ViewportTile.h/.cpp` — the per-tile rendering logic extracted from `ViewportPanel::Render()`.
|
||||
- **New files**: `src/ui/ViewportLayout.h/.cpp` — layout splitting/merge logic, divider drag, layout persistence.
|
||||
- Memory: N `UsdSceneRenderer` instances means N FBO draw targets. For N≤4 and typical 1080p viewports this is ~200 MB worst-case — acceptable for a DCC tool.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Transform manipulator renders only in the focused viewport tile
|
||||
The transform gizmo (manipulator) SHALL render and accept input only in the tile that was most recently clicked. All other tiles SHALL NOT display the gizmo.
|
||||
|
||||
#### Scenario: Focused tile shows gizmo, other tile does not
|
||||
- **WHEN** the user clicks in tile A (making it focused), then selects a prim
|
||||
- **THEN** the transform gizmo appears over the selection in tile A only; tile B shows no gizmo
|
||||
|
||||
#### Scenario: Move focus to another tile
|
||||
- **WHEN** the user clicks in tile B while tile A was focused
|
||||
- **THEN** the gizmo disappears from tile A and appears in tile B over the selection
|
||||
|
||||
### Requirement: Manipulator mode and space are global
|
||||
The manipulator tool mode (Select/Move/Rotate/Scale) and transform space (World/Object) SHALL be global settings shared across all tiles, owned by the `ViewportPanel` container.
|
||||
|
||||
#### Scenario: Change manipulator mode in focused tile
|
||||
- **WHEN** the user presses W (Move) while tile A is focused
|
||||
- **THEN** the mode switches to Move globally, and if the user clicks in tile B, tile B's gizmo is in Move mode
|
||||
|
||||
### Requirement: Keyboard shortcuts route to focused tile
|
||||
Keyboard shortcuts for the manipulator (Q/W/E/R), frame selection (F), frame all (A), and maximize toggle (Space) SHALL only apply to the focused tile.
|
||||
|
||||
#### Scenario: Frames selection in focused tile only
|
||||
- **WHEN** the user presses F while tile A is focused
|
||||
- **THEN** tile A's camera frames the selection; other tiles' cameras remain unchanged
|
||||
|
||||
#### Scenario: Tool shortcut changes global mode
|
||||
- **WHEN** the user presses W while tile A is focused
|
||||
- **THEN** the global manipulator mode changes to Move, and the focused tile (A) shows the Move gizmo if a prim is selected
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Independent camera per viewport tile
|
||||
Each viewport tile SHALL own an independent `ViewportCamera` instance. Camera operations (orbit, pan, dolly, zoom, frame) in one tile SHALL NOT affect any other tile's camera.
|
||||
|
||||
#### Scenario: Orbit in one tile, other tiles unchanged
|
||||
- **WHEN** the user Alt+LMB drags to orbit in tile A
|
||||
- **THEN** tile A's camera rotates, while tiles B, C, D remain at their previous camera positions
|
||||
|
||||
#### Scenario: Frame selection in one tile
|
||||
- **WHEN** the user presses F in a tile
|
||||
- **THEN** that tile's camera frames the current selection, other tiles are unaffected
|
||||
|
||||
### Requirement: Per-tile camera selector dropdown
|
||||
Each tile SHALL have a compact camera selector (icon + dropdown) showing "Free Camera" and all USD camera prims on the stage. Selecting a USD camera in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: Tile A uses Free Camera, Tile B uses a USD camera prim
|
||||
- **WHEN** the user selects a USD camera prim in tile B's camera dropdown
|
||||
- **THEN** tile B switches to USD camera mode and renders from that camera's view, while tile A continues in free camera mode
|
||||
|
||||
#### Scenario: Both tiles use the same USD camera
|
||||
- **WHEN** the user selects the same USD camera prim in both tile A and tile B
|
||||
- **THEN** both tiles render from that camera's view but can navigate independently when one tile goes into free camera mode
|
||||
|
||||
### Requirement: Camera list refreshes on dropdown open
|
||||
Each tile SHALL refresh its camera prim list from the stage whenever its camera dropdown is opened.
|
||||
|
||||
#### Scenario: New camera added, then dropdown opened
|
||||
- **WHEN** the user adds a new UsdGeomCamera prim to the stage and opens a tile's camera dropdown
|
||||
- **THEN** the dropdown includes the newly added camera prim
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Per-tile grid toggle
|
||||
Each viewport tile SHALL have an independent grid visibility setting. Toggling the grid in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: Grid on in Tile A, off in Tile B
|
||||
- **WHEN** the user toggles grid ON in tile A and OFF in tile B
|
||||
- **THEN** tile A shows the ground grid and tile B does not
|
||||
|
||||
### Requirement: Per-tile anti-aliasing toggle
|
||||
Each viewport tile SHALL have an independent line-AA setting. Toggling AA in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: AA on in Tile A, off in Tile B
|
||||
- **WHEN** the user toggles AA ON in tile A and OFF in tile B
|
||||
- **THEN** tile A renders line overlays with GL_LINE_SMOOTH, tile B renders without
|
||||
|
||||
### Requirement: Per-tile background color
|
||||
Each viewport tile SHALL have an independent background color. Changing the background color in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: Two tiles show different background colors
|
||||
- **WHEN** the user sets tile A's background to "Black" and tile B's background to "Dark Gray"
|
||||
- **THEN** tile A renders with a black background and tile B with a dark gray background
|
||||
|
||||
### Requirement: Per-tile bounding box display
|
||||
Each viewport tile SHALL have an independent bounding box display mode (None / Per Object / All Selection). Changing the bbox mode in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: BBox mode differs between tiles
|
||||
- **WHEN** the user sets tile A to "Per Object" bbox mode and tile B to "None"
|
||||
- **THEN** tile A draws bounding boxes on each selected prim, tile B draws none
|
||||
|
||||
### Requirement: Per-tile render delegate
|
||||
Each viewport tile SHALL support an independent render delegate selection. Changing the render delegate in one tile SHALL NOT affect other tiles.
|
||||
|
||||
#### Scenario: Storm in Tile A, HdEmbree in Tile B
|
||||
- **WHEN** the user selects "HdStorm" for tile A and "HdEmbree" for tile B
|
||||
- **THEN** tile A renders via HdStorm and tile B via HdEmbree
|
||||
|
||||
### Requirement: Compact per-tile toolbar
|
||||
Each tile's toolbar SHALL use compact icon-only buttons (16×16) arranged in a single horizontal row above the rendered image, consuming no more than 32px height. Tooltips SHALL be available on hover.
|
||||
|
||||
#### Scenario: Toolbar renders compactly in a small tile
|
||||
- **WHEN** a tile is 300px wide in an HSplit layout
|
||||
- **THEN** the toolbar fits within the tile width without clipping, using a horizontal scroll or overflow dropdown if necessary
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Shared selection across all viewport tiles
|
||||
Selection SHALL be shared across all viewport tiles. When a prim is picked in any tile (single click or rect drag), all tiles SHALL update their selection highlight display to show the same set of selected prims.
|
||||
|
||||
#### Scenario: Pick in Tile A, Tile B shows highlight
|
||||
- **WHEN** the user single-clicks a prim in tile A
|
||||
- **THEN** tile A and tile B both highlight that prim with the selection overlay
|
||||
|
||||
#### Scenario: Rect select in Tile B, Tile A updates
|
||||
- **WHEN** the user rect-drags over multiple prims in tile B
|
||||
- **THEN** both tile A and tile B highlight the same set of prims
|
||||
|
||||
### Requirement: Selection change propagates to SceneHierarchyPanel and PropertyPanel
|
||||
When selection changes via any viewport tile, the `OnPrimPicked` / `OnPrimsPickedRect` callbacks SHALL fire from the `ViewportPanel` container, just as they do today.
|
||||
|
||||
#### Scenario: Pick in viewport updates hierarchy
|
||||
- **WHEN** the user picks a prim in any tile
|
||||
- **THEN** the SceneHierarchyPanel and PropertyPanel update to show the selected prim's properties
|
||||
|
||||
### Requirement: Shift+click additive selection across tiles
|
||||
Shift+click SHALL work consistently across all tiles. Prim picked in any tile is added to or removed from the shared selection set.
|
||||
|
||||
#### Scenario: Shift+click in Tile A, then Tile B
|
||||
- **WHEN** the user selects prim P1 in tile A, then Shift+clicks prim P2 in tile B
|
||||
- **THEN** both P1 and P2 are in the shared selection, highlighted in both tiles
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Layout presets
|
||||
The viewport container SHALL support switching between four layout presets at any time: Single (1 tile), HSplit (2 tiles side-by-side), VSplit (2 tiles top-bottom), and Quad (4 tiles in a 2×2 grid).
|
||||
|
||||
#### Scenario: Switch layout from Single to HSplit
|
||||
- **WHEN** the user selects "Horizontal Split" from the layout menu
|
||||
- **THEN** the viewport splits into two equal-sized tiles side-by-side
|
||||
|
||||
#### Scenario: Switch layout from HSplit to Single
|
||||
- **WHEN** the user selects "Single" from the layout menu
|
||||
- **THEN** the two tiles merge back into a single tile, and the focused tile's camera/settings are preserved
|
||||
|
||||
#### Scenario: Switch from HSplit to Quad
|
||||
- **WHEN** the user switches from HSplit to Quad
|
||||
- **THEN** four tiles are created in a 2×2 grid, filling the viewport area
|
||||
|
||||
### Requirement: Draggable dividers
|
||||
The divider line between tiles SHALL be draggable to resize adjacent tiles. Dividers SHALL be 6px wide with visual hover feedback.
|
||||
|
||||
#### Scenario: Drag horizontal divider
|
||||
- **WHEN** the user clicks and drags the divider between two tiles
|
||||
- **THEN** the divider follows the mouse, and tiles resize proportionally
|
||||
|
||||
#### Scenario: Drag divider to edge
|
||||
- **WHEN** the user drags a divider to within 10% of the viewport edge
|
||||
- **THEN** the divider snaps back to the 10% boundary to prevent tiles from becoming too small (minimum 100px per dimension)
|
||||
|
||||
### Requirement: Layout applies to the entire viewport area
|
||||
When the layout changes, all tiles SHALL be redistributed to fill the entire available viewport panel content region. No gaps between tiles.
|
||||
|
||||
#### Scenario: Resize main window with Quad layout
|
||||
- **WHEN** the user resizes the main application window
|
||||
- **THEN** all four tiles resize proportionally to maintain their relative sizes and fill the viewport content area
|
||||
|
||||
### Requirement: Space-key maximize restores previous layout
|
||||
When maximized, pressing Space again SHALL restore the exact layout and divider positions from before maximization.
|
||||
|
||||
#### Scenario: Maximize and restore preserves layout
|
||||
- **WHEN** the user has an HSplit layout with divider at 0.3, then hovers tile B and presses Space, then presses Space again
|
||||
- **THEN** the viewport returns to HSplit layout with both tiles visible and the divider at 0.3
|
||||
|
||||
### Requirement: Escape restores maximized viewport
|
||||
When maximized, pressing Escape SHALL restore the previous layout (same behavior as Space toggle).
|
||||
|
||||
#### Scenario: Escape during maximize
|
||||
- **WHEN** the viewport is maximized and the user presses Escape
|
||||
- **THEN** the previous multi-tile layout is restored
|
||||
|
||||
### Requirement: Space requires tile hover in multi-tile layouts
|
||||
In Single layout (only 1 tile), Space SHALL be a no-op — there is nothing to maximize.
|
||||
|
||||
#### Scenario: Space ignored in Single layout
|
||||
- **WHEN** the viewport is already in Single layout and the user presses Space
|
||||
- **THEN** the viewport remains unchanged in Single layout
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
## 1. Extract ViewportTile class
|
||||
|
||||
- [x] 1.1 Create `src/ui/ViewportTile.h` with class declaration — owns `ViewportCamera`, `UsdSceneRenderer`, per-tile settings (grid, AA, bbox mode, bg color, render delegate, camera index), and a pointer to the shared selection + manipulator
|
||||
- [x] 1.2 Create `src/ui/ViewportTile.cpp` — extract per-tile rendering logic from `ViewportPanel::Render()`: camera resolution, scene rendering, overlay rendering (axis, bbox, camera wireframes), DrawSelectionRect, RenderManipulatorOverlay, RenderContextMenu, HandleInput
|
||||
- [x] 1.3 Verify tile renders standalone by temporarily instantiating one ViewportTile in Application
|
||||
|
||||
## 2. Refactor ViewportPanel into container
|
||||
|
||||
- [x] 2.1 Rewrite `ViewportPanel` header to own `std::vector<std::unique_ptr<ViewportTile>>`, `TransformManipulator`, shared selection state (`m_selectedSdfPaths`, `m_selectedPrimPath`), layout enum (`LayoutMode`), divider normalized positions, and maximize state (`m_maximizedTileIndex`, `m_layoutBeforeMaximize`)
|
||||
- [x] 2.2 Rewrite `ViewportPanel::Render()` to compute tile rectangles from layout state, iterate tiles calling `ViewportTile::Render()` + `ViewportTile::HandleInput()`, and draw dividers. When maximized, render only the maximized tile at full viewport area (no dividers)
|
||||
- [x] 2.3 Add `ViewportPanel::SetLayout(LayoutMode)` — creates/destroys tiles as needed, preserves focused tile's camera/settings when possible. Clear `m_maximizedTileIndex` on explicit layout change
|
||||
- [x] 2.4 Add divider-drag handling in `ViewportPanel::Render()` — hit-test divider regions, update normalized positions on drag. Skip divider hit-test when maximized
|
||||
- [x] 2.5 Add `ViewportPanel::SetLayoutMenu()` — a layout menu in the viewport title bar or right-click context menu
|
||||
- [x] 2.6 Add Space-key maximize/restore: detect Space key press (guarded by `!io.WantTextInput` and `LayoutMode != Single`), save pre-maximize layout, set `m_maximizedTileIndex` to the hovered tile's index. On second Space or Escape, restore `m_maximizedTileIndex` to -1 and reload saved layout. Add visual indicator (border glow or text label) on maximized tile
|
||||
- [x] 2.7 Expose `ViewportPanel::GetFocusedTileIndex()` and wire up `OnPrimPicked` / `OnPrimsPickedRect` from the focused tile to the shared selection callbacks. When maximized, the maximized tile is implicitly the focused tile
|
||||
|
||||
## 3. Implement shared selection
|
||||
|
||||
- [x] 3.1 Store `m_selectedSdfPaths` and `m_selectedPrimPath` on `ViewportPanel` (container)
|
||||
- [x] 3.2 On pick in any tile, update shared selection, then broadcast `m_renderer.SetSelectedPaths()` to all tiles
|
||||
- [x] 3.3 Fire `OnPrimPicked` / `OnPrimsPickedRect` from the container (not tiles) so downstream panels see a single source of truth
|
||||
|
||||
## 4. Implement focused-viewport manipulator
|
||||
|
||||
- [x] 4.1 Track `m_focusedTileIndex` on `ViewportPanel` — updated on LMB click in any tile
|
||||
- [x] 4.2 Only call `m_manipulator.Render()` and `m_manipulator.HandleInput()` for the focused tile
|
||||
- [x] 4.3 Wire keyboard shortcuts (Q/W/E/R) through the container to the global manipulator, and F/A to the focused tile's camera. Ensure Space maximize toggle works correctly when a tile is focused (not consumed by manipulator)
|
||||
|
||||
## 5. Add per-tile compact toolbar
|
||||
|
||||
- [x] 5.1 Implement compact icon-only toolbar in `ViewportTile::RenderToolbar()` — camera selector + render delegate button + grid/AA/bbox icons, all 16×16 with tooltips
|
||||
- [ ] 5.2 Add overflow handling: if toolbar exceeds tile width, collapse into a "..." dropdown menu
|
||||
|
||||
## 6. Wire up Application integration
|
||||
|
||||
- [x] 6.1 Update `Application::Initialize()` — `m_viewportPanel` still works, `SetStage` broadcasts to all tiles, `SetSelectedPrimPath` updates shared selection
|
||||
- [x] 6.2 Update `Application::RenderUI()` — ensure the Viewport window renders correctly with the new container layout
|
||||
- [ ] 6.3 Test all existing interaction flows: stage open/close, camera switching, selection sync, manipulator tools, context menu, frame shortcuts
|
||||
|
||||
## 7. Build and test
|
||||
|
||||
- [x] 7.1 Configure with `cmake --preset default`
|
||||
- [x] 7.2 Build Release with `cmake --build build --config Release` — fix any compilation errors
|
||||
- [x] 7.3 Install with `cmake --install build --config Release`
|
||||
- [ ] 7.4 Run `App.exe` and visually verify: single viewport works, switch to HSplit/VSplit/Quad, camera independence, shared selection, manipulator in focused tile only, divider dragging, Space-key maximize/restore with correct layout preservation, Escape restore, maximize in Quad layout, Space no-op in Single layout
|
||||
- [ ] 7.5 Run CTest: `ctest --test-dir build -C Release` — fix any test failures
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-25
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
## Context
|
||||
|
||||
The codebase is a C++17 Windows desktop application using OpenUSD + ImGui + OpenGL. All scene mutations are applied immediately to the live `UsdStageRefPtr` with no reversal mechanism. Edits come from five surfaces: (1) viewport gizmo drag (`TransformManipulator`), (2) Property panel TRS fields (`PropertyPanel`), (3) scene hierarchy context menus (`SceneHierarchyPanel` — create/delete/ref), (4) generic attribute edits (`PropertyManager`), and (5) layer CRUD (`LayerPanel`/`LayerManager`). There is currently no command pattern, history stack, or deferred-dispatch infrastructure.
|
||||
|
||||
USD provides `SdfLayer`-level undo via `SdfLayer::BeginChangeBlock` / `EndChangeBlock` + `SdfLayer::DumpLayerInfo`, but it does not expose a first-class undo stack accessible to application code. The simpler and more reliable approach for this scope is application-level command objects that store enough pre/post state to replay or reverse themselves.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Single-level command history with unlimited depth (bounded only by memory).
|
||||
- `Ctrl+Z` / `Ctrl+Y` / `Ctrl+Shift+Z` hotkeys processed in the main ImGui loop.
|
||||
- **Edit → Undo / Redo** menu items that grey out when the respective stack end is reached.
|
||||
- All five edit surfaces wrapped in reversible commands.
|
||||
- Gizmo drag treated as one atomic command (not a command per frame).
|
||||
- History cleared on stage open/close/new.
|
||||
|
||||
**Non-Goals:**
|
||||
- Branching undo trees.
|
||||
- Persisting history across sessions.
|
||||
- Undoing camera movement.
|
||||
- Undoing layer mute/unmute (mute state is cosmetic / non-destructive; deferred to a follow-up).
|
||||
- Collaborative / multi-user conflict resolution.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — Classic Command Pattern over USD change-block replay
|
||||
|
||||
**Decision**: Store pre/post value snapshots in application-level `ICommand` objects; do not attempt to record and replay USD change blocks.
|
||||
|
||||
**Rationale**: USD's change notification system is designed for hydra update propagation, not user-facing undo. Reconstructing "what changed" from `SdfNotice` events is complex and fragile. Application-level commands with explicit `Undo()` / `Execute()` are simpler, debuggable, and already the standard approach in DCC tools.
|
||||
|
||||
**Alternatives considered**: `SdfLayer::StateDelegate` (undocumented, internal), `UsdStage::GetMutedLayers` diffing (covers only mute state).
|
||||
|
||||
---
|
||||
|
||||
### D2 — `CommandHistory` owned by `Application`, passed as pointer to subsystems
|
||||
|
||||
**Decision**: `Application` owns a single `CommandHistory` instance. Panels and managers receive a raw `CommandHistory*` alongside existing manager pointers. No global singleton.
|
||||
|
||||
**Rationale**: Mirrors the existing pattern for managers (raw pointers, no DI framework). Avoids threading concerns (single-threaded ImGui loop). A singleton would make testing harder.
|
||||
|
||||
---
|
||||
|
||||
### D3 — Gizmo drag commits one command on mouse-button-release
|
||||
|
||||
**Decision**: `TransformManipulator` stores drag-start TRS when drag begins (`m_isDragging` transitions false→true). On drag-end (mouse released), it pushes a single `TransformCommand(prim, startTRS, endTRS)` to `CommandHistory`.
|
||||
|
||||
**Rationale**: The current code already accumulates delta from drag-start each frame (`m_dragStartTranslate/Rotate/Scale`) instead of applying per-frame deltas, so the start state is already cached. A single command per gesture is the correct granularity for a user (one Undo reverses one drag, not 60 frames of micro-moves).
|
||||
|
||||
**Change required**: `TransformManipulator` needs a `CommandHistory*` member set during construction/init; on drag-end it pushes the command instead of — or after — the final `Apply*Delta`.
|
||||
|
||||
---
|
||||
|
||||
### D4 — Snapshot-based commands for attribute edits
|
||||
|
||||
**Decision**: `AttributeSetCommand<T>` stores `(UsdAttribute, T oldValue, T newValue, UsdTimeCode)`. `Undo()` calls `attr.Set(oldValue, timeCode)`, `Execute()` / `Redo()` calls `attr.Set(newValue, timeCode)`.
|
||||
|
||||
**Rationale**: USD attributes hold typed values; snapshot is cheap for scalar/vector types. The full type-erase is handled via `std::function<void()>` closures rather than a deep template hierarchy, keeping the concrete command count manageable.
|
||||
|
||||
**Command structure**:
|
||||
```cpp
|
||||
struct AttributeSetCommand : ICommand {
|
||||
std::string description;
|
||||
std::function<void()> executeFunc; // captures new value
|
||||
std::function<void()> undoFunc; // captures old value
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D5 — Prim create/delete commands use path + type + layer targeting
|
||||
|
||||
**Decision**: `CreatePrimCommand` stores `(SdfPath, TfToken typeName, SdfLayerHandle targetLayer)`. `Undo()` removes the prim. `DeletePrimCommand` stores the full serialized prim spec via `SdfLayer::ExportToString` scoped to that prim, and `Undo()` re-imports it.
|
||||
|
||||
**Rationale**: Prim deletion must round-trip the full spec (attributes, relationships, metadata). `SdfLayer::ExportToString` + `SdfLayer::ImportFromString` provides safe serialization without custom recursion.
|
||||
|
||||
---
|
||||
|
||||
### D6 — Layer operations use index-based snapshots
|
||||
|
||||
**Decision**: `LayerReorderCommand` stores the full ordered layer-path list before and after; `Undo()` restores the prior list. `LayerCreateCommand` and `LayerRemoveCommand` store the layer file path + insertion index.
|
||||
|
||||
**Rationale**: Layer order is a simple list; full-list snapshots are small and unambiguous. Per-swap tracking would be more complex for multi-step reorders.
|
||||
|
||||
---
|
||||
|
||||
### D7 — Stack invalidation on stage lifecycle events
|
||||
|
||||
**Decision**: `CommandHistory::Clear()` is called from `Application::RefreshManagers()` (already invoked on open/close/new). No special hook needed.
|
||||
|
||||
**Rationale**: Commands hold `UsdAttribute`, `UsdPrim`, and `SdfLayerHandle` references that become dangling or point to a different stage after a stage reload. Clearing is the only safe option.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] USD prim/attribute handles become stale** if the user opens a different stage between Undo calls.
|
||||
→ Mitigation: `Clear()` on every stage lifecycle event (D7).
|
||||
|
||||
- **[Risk] `SdfLayer::ExportToString` for large prims may be slow**.
|
||||
→ Mitigation: Export is done only at delete time (not every frame). Acceptable for scene-level operations.
|
||||
|
||||
- **[Risk] `TransformManipulator` drag-end detection** requires distinguishing "drag just ended this frame" from "not dragging". Currently `m_isDragging` is set to false before `HandleInput` returns; the transition must be captured.
|
||||
→ Mitigation: Detect `wasDrawing && !m_isDragging` transition within `HandleInput` to push the command at the right moment.
|
||||
|
||||
- **[Risk] Property panel writes TRS continuously as user types** (no commit-on-enter today). Pushing a command per keystroke floods the history.
|
||||
→ Mitigation: Add a deferred-commit pattern (push command on `ImGui::IsItemDeactivatedAfterEdit()`), matching standard DCC behaviour.
|
||||
|
||||
- **[Risk] Multi-attribute edits (e.g., all three translate components at once)** would push three commands.
|
||||
→ Mitigation: Acceptable for now; a `MacroCommand` (composite) can be added later if needed.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Introduce `ICommand` / `CommandHistory` as new files with no side effects on existing code.
|
||||
2. Thread `CommandHistory*` through constructors/init methods of each subsystem (additive changes, no existing interface removed).
|
||||
3. Wrap each edit surface one at a time (Transform → Property → SceneHierarchy → Attribute → Layer). Build after each group.
|
||||
4. Add hotkeys and menu items last (safest to add once all commands are wired).
|
||||
5. No data migration needed — history is in-memory only.
|
||||
|
||||
**Rollback**: Removing the `CommandHistory*` parameter and the `Push()` calls restores pre-change behaviour; no persisted format to migrate.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `Ctrl+Z` also undo layer **mute/unmute**? Currently scoped out (Non-Goal), but the architecture supports it trivially. Revisit if stakeholders request it.
|
||||
- Should there be a **maximum history depth** (e.g., 200 steps) to cap memory? Not required at this scope; can be added to `CommandHistory` later as a constructor parameter.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
## Why
|
||||
|
||||
The application currently applies all edits (transforms, attribute changes, prim creation/deletion, layer operations) directly to the live USD stage with no reversal mechanism, making it impossible to correct mistakes without closing and reopening the scene. Undo/redo is a foundational feature expected in any scene editor, and its absence is a critical usability gap.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a `CommandHistory` class (command pattern) that records reversible `ICommand` objects.
|
||||
- Wrap all mutating operations — prim create, prim delete, add/replace reference, transform gizmo drag, property panel TRS edits, arbitrary attribute edits, and layer management operations — in concrete `ICommand` subclasses with `Execute()` / `Undo()` pairs.
|
||||
- Expose `Ctrl+Z` / `Ctrl+Y` (and `Ctrl+Shift+Z`) global hotkeys processed in the main loop.
|
||||
- Integrate gizmo drag commits: the `TransformManipulator` currently emits continuous per-frame deltas; it will instead emit a single command on drag-end so that one undo step reverses the entire drag.
|
||||
- Add **Edit → Undo / Redo** menu items with greyed-out state when the stack is empty.
|
||||
- Clear the history stack on stage close / new stage open (invalid USD references cannot be safely replayed).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `command-history`: Core undo/redo stack — `ICommand` interface, `CommandHistory` manager, hotkey dispatch, and menu integration.
|
||||
- `undoable-transform-edits`: Commands wrapping `UsdGeomXformCommonAPI` writes from both the viewport gizmo and the Property panel.
|
||||
- `undoable-scene-edits`: Commands wrapping prim creation, prim deletion, add-reference, and replace-reference operations.
|
||||
- `undoable-attribute-edits`: Command wrapping arbitrary `UsdAttribute::Set<T>` calls from `PropertyManager`.
|
||||
- `undoable-layer-ops`: Commands wrapping `LayerManager` sublayer create, remove, reorder, and mute/unmute operations.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_(none — no existing spec-level requirements are being tightened or relaxed)_
|
||||
|
||||
## Impact
|
||||
|
||||
- **New file**: `src/core/CommandHistory.h/.cpp` — `ICommand`, `CommandHistory`.
|
||||
- **New files**: `src/core/commands/` — one `.h/.cpp` pair per concrete command group.
|
||||
- **Modified**: `src/ui/TransformManipulator.cpp` — capture drag-start state, emit command on drag-end instead of applying incremental deltas.
|
||||
- **Modified**: `src/ui/PropertyPanel.cpp` — wrap TRS writes in commands.
|
||||
- **Modified**: `src/ui/SceneHierarchyPanel.cpp` — wrap create/delete/ref operations in commands.
|
||||
- **Modified**: `src/core/PropertyManager.cpp` — wrap `SetPropertyValue` in command.
|
||||
- **Modified**: `src/ui/LayerPanel.cpp` — wrap layer CRUD in commands.
|
||||
- **Modified**: `src/ui/Application.cpp` — add `CommandHistory` member, hotkey handling, Edit menu.
|
||||
- **Modified**: `CMakeLists.txt` — add new source files.
|
||||
- No new external dependencies.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: ICommand interface
|
||||
The system SHALL provide an `ICommand` pure-virtual interface with `Execute()`, `Undo()`, and `GetDescription()` methods that all reversible operations implement.
|
||||
|
||||
#### Scenario: Execute runs the operation
|
||||
- **WHEN** `ICommand::Execute()` is called on a newly created command
|
||||
- **THEN** the operation is applied to the USD stage and the scene reflects the new state
|
||||
|
||||
#### Scenario: Undo reverses the operation
|
||||
- **WHEN** `ICommand::Undo()` is called after `Execute()`
|
||||
- **THEN** the USD stage returns to the state it was in before `Execute()` was called
|
||||
|
||||
### Requirement: CommandHistory stack management
|
||||
The system SHALL maintain two stacks (undo stack, redo stack). `Push(cmd)` executes the command, pushes it onto the undo stack, and clears the redo stack. `Undo()` pops from the undo stack, calls `Undo()` on the command, and pushes it onto the redo stack. `Redo()` pops from the redo stack, calls `Execute()` on the command, and pushes it back onto the undo stack.
|
||||
|
||||
#### Scenario: Push clears redo stack
|
||||
- **WHEN** the user undoes two steps and then makes a new edit
|
||||
- **THEN** the redo stack is cleared and the new command is the top of the undo stack
|
||||
|
||||
#### Scenario: Undo with empty stack is a no-op
|
||||
- **WHEN** `CommandHistory::Undo()` is called and the undo stack is empty
|
||||
- **THEN** no crash occurs and the scene is unchanged
|
||||
|
||||
#### Scenario: Redo with empty stack is a no-op
|
||||
- **WHEN** `CommandHistory::Redo()` is called and the redo stack is empty
|
||||
- **THEN** no crash occurs and the scene is unchanged
|
||||
|
||||
### Requirement: Ctrl+Z / Ctrl+Y hotkeys
|
||||
The application SHALL process `Ctrl+Z` to invoke `Undo()` and `Ctrl+Y` (and `Ctrl+Shift+Z`) to invoke `Redo()` during the ImGui main loop, unless an ImGui text input widget has keyboard focus.
|
||||
|
||||
#### Scenario: Ctrl+Z triggers undo
|
||||
- **WHEN** the user presses `Ctrl+Z` and the undo stack is non-empty
|
||||
- **THEN** the most recent command is undone and the viewport reflects the reverted state
|
||||
|
||||
#### Scenario: Ctrl+Y triggers redo
|
||||
- **WHEN** the user presses `Ctrl+Y` and the redo stack is non-empty
|
||||
- **THEN** the most recent undone command is reapplied and the viewport reflects the restored state
|
||||
|
||||
#### Scenario: Hotkeys ignored in text inputs
|
||||
- **WHEN** an ImGui `InputText` widget has keyboard focus and the user presses `Ctrl+Z`
|
||||
- **THEN** ImGui handles the keypress as text-widget undo and `CommandHistory::Undo()` is NOT called
|
||||
|
||||
### Requirement: Edit menu Undo/Redo items
|
||||
The application SHALL provide **Edit → Undo** and **Edit → Redo** menu items. Each item SHALL display the description of the command that would be affected. Items SHALL be greyed out (disabled) when the respective stack is empty.
|
||||
|
||||
#### Scenario: Undo item shows command description
|
||||
- **WHEN** the undo stack is non-empty and the Edit menu is opened
|
||||
- **THEN** the Undo item reads "Undo: <description of top command>" and is enabled
|
||||
|
||||
#### Scenario: Undo item disabled when stack empty
|
||||
- **WHEN** the undo stack is empty and the Edit menu is opened
|
||||
- **THEN** the Undo item is greyed out and clicking it has no effect
|
||||
|
||||
### Requirement: History cleared on stage lifecycle events
|
||||
The system SHALL call `CommandHistory::Clear()` whenever a stage is opened, closed, or replaced, so that stale USD object references cannot be dereferenced.
|
||||
|
||||
#### Scenario: History clears on stage open
|
||||
- **WHEN** the user opens a new USD file
|
||||
- **THEN** both undo and redo stacks are empty after the stage loads
|
||||
|
||||
#### Scenario: History clears on stage close
|
||||
- **WHEN** the user closes the current stage
|
||||
- **THEN** both undo and redo stacks are empty
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Generic attribute set is undoable
|
||||
The system SHALL push an `AttributeSetCommand` to `CommandHistory` when `PropertyManager::SetPropertyValue` or `SetPropertyValueInLayer` is called. The command SHALL capture the old value (read before the set) and the new value as `std::function<void()>` closures so it is type-agnostic. `Undo()` SHALL restore the old value; `Redo()` SHALL re-apply the new value.
|
||||
|
||||
#### Scenario: Undo reverts attribute change
|
||||
- **WHEN** the user changes a light's intensity via the Property panel and then presses Ctrl+Z
|
||||
- **THEN** the intensity returns to its previous value
|
||||
|
||||
#### Scenario: Redo re-applies attribute change
|
||||
- **WHEN** the user undoes an attribute change and then presses Ctrl+Y
|
||||
- **THEN** the attribute is set back to the edited value
|
||||
|
||||
### Requirement: Attribute command preserves layer targeting
|
||||
The `AttributeSetCommand` SHALL record which `SdfLayerHandle` was the active edit target. `Undo()` and `Redo()` SHALL use `UsdEditContext` to direct the attribute write to that same layer.
|
||||
|
||||
#### Scenario: Undo writes revert to correct layer
|
||||
- **WHEN** the user edits an attribute with a non-root layer selected and then undoes
|
||||
- **THEN** the revert opinion is written to the same non-root layer, not the root layer
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Sublayer creation is undoable
|
||||
The system SHALL push a `LayerCreateCommand` to `CommandHistory` when a new sublayer is created via the Layer panel. `Undo()` SHALL remove the sublayer from the layer stack. `Redo()` SHALL re-add it at the same position.
|
||||
|
||||
#### Scenario: Undo removes created sublayer
|
||||
- **WHEN** the user creates a sublayer and then presses Ctrl+Z
|
||||
- **THEN** the sublayer is no longer in the layer stack
|
||||
|
||||
### Requirement: Sublayer removal is undoable
|
||||
The system SHALL push a `LayerRemoveCommand` to `CommandHistory` when a sublayer is removed via the Layer panel. `Undo()` SHALL re-insert the layer path at its original index. `Redo()` SHALL remove it again.
|
||||
|
||||
#### Scenario: Undo restores removed sublayer
|
||||
- **WHEN** the user removes a sublayer and then presses Ctrl+Z
|
||||
- **THEN** the sublayer reappears at its original position in the stack
|
||||
|
||||
### Requirement: Sublayer reorder is undoable
|
||||
The system SHALL push a `LayerReorderCommand` to `CommandHistory` when sublayers are reordered (moved up or moved down) in the Layer panel. The command SHALL store the full ordered list before and after. `Undo()` SHALL restore the previous order.
|
||||
|
||||
#### Scenario: Undo reverts move-up
|
||||
- **WHEN** the user moves a sublayer up and then presses Ctrl+Z
|
||||
- **THEN** the layer returns to its previous position in the stack
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Prim creation is undoable
|
||||
The system SHALL push a `CreatePrimCommand` to `CommandHistory` when a prim is created via the scene hierarchy context menu. `Undo()` SHALL remove the created prim from the stage. `Redo()` SHALL re-create the prim with the same path and type.
|
||||
|
||||
#### Scenario: Undo removes created prim
|
||||
- **WHEN** the user creates a Sphere prim and then presses Ctrl+Z
|
||||
- **THEN** the Sphere prim no longer appears in the scene hierarchy
|
||||
|
||||
#### Scenario: Redo re-creates the prim
|
||||
- **WHEN** the user undoes a prim creation and then presses Ctrl+Y
|
||||
- **THEN** the Sphere prim reappears at the same path with the same type
|
||||
|
||||
### Requirement: Prim deletion is undoable
|
||||
The system SHALL push a `DeletePrimCommand` to `CommandHistory` when a prim is deleted via the scene hierarchy confirm modal. Before deletion, the command SHALL serialize the prim spec (including all authored opinions on the edit-target layer) to an in-memory string. `Undo()` SHALL restore the serialized spec to the layer.
|
||||
|
||||
#### Scenario: Undo restores deleted prim
|
||||
- **WHEN** the user deletes a prim and then presses Ctrl+Z
|
||||
- **THEN** the prim reappears in the scene hierarchy with all its authored attributes intact
|
||||
|
||||
#### Scenario: Redo re-deletes the prim
|
||||
- **WHEN** the user undoes a deletion and then presses Ctrl+Y
|
||||
- **THEN** the prim is deleted again
|
||||
|
||||
### Requirement: Add-reference is undoable
|
||||
The system SHALL push an `AddReferenceCommand` to `CommandHistory` when an external USD file is added as a reference. `Undo()` SHALL remove the reference (and the wrapping Xform prim if it was created by the add-reference operation). `Redo()` SHALL re-add the reference.
|
||||
|
||||
#### Scenario: Undo removes added reference
|
||||
- **WHEN** the user adds a reference to an external file and then presses Ctrl+Z
|
||||
- **THEN** the reference prim is removed from the scene hierarchy
|
||||
|
||||
### Requirement: Replace-reference is undoable
|
||||
The system SHALL push a `ReplaceReferenceCommand` to `CommandHistory` when an existing reference is replaced via the scene hierarchy. `Undo()` SHALL restore the previous `SdfReference`. `Redo()` SHALL apply the replacement again.
|
||||
|
||||
#### Scenario: Undo restores previous reference path
|
||||
- **WHEN** the user replaces a reference and then presses Ctrl+Z
|
||||
- **THEN** the prim points back to the original reference file
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Gizmo drag produces one atomic undo command
|
||||
The system SHALL record the pre-drag TRS state when a viewport gizmo drag begins, and push a single `TransformCommand` to `CommandHistory` when the drag ends (mouse button released). The command SHALL store the prim path, the edit-target layer, and the full TRS (translate, rotate, scale) snapshots from before and after the drag.
|
||||
|
||||
#### Scenario: Single undo reverses entire drag
|
||||
- **WHEN** the user drags the Move gizmo to translate a prim and then presses Ctrl+Z
|
||||
- **THEN** the prim returns to the position it had before the drag started (not an intermediate position)
|
||||
|
||||
#### Scenario: Redo restores drag result
|
||||
- **WHEN** the user undoes a gizmo drag and then presses Ctrl+Y
|
||||
- **THEN** the prim moves back to the post-drag position
|
||||
|
||||
#### Scenario: No command pushed for zero-delta drag
|
||||
- **WHEN** the user clicks a gizmo axis but releases without moving
|
||||
- **THEN** no command is pushed to the history stack
|
||||
|
||||
### Requirement: Property panel TRS commits produce undo commands
|
||||
The system SHALL push a `TransformCommand` to `CommandHistory` when a translate, rotate, or scale field in the Property panel loses focus after being edited (`ImGui::IsItemDeactivatedAfterEdit()`). The command SHALL store the pre-edit and post-edit TRS values.
|
||||
|
||||
#### Scenario: Undo reverts property-panel translate edit
|
||||
- **WHEN** the user types a new X translate value in the Property panel, presses Tab to commit, then presses Ctrl+Z
|
||||
- **THEN** the prim's translate returns to the value it had before the edit
|
||||
|
||||
#### Scenario: Typing without committing does not push a command
|
||||
- **WHEN** the user begins editing a translate field but presses Escape to cancel
|
||||
- **THEN** no new command is added to the undo stack
|
||||
|
||||
### Requirement: Transform command respects the active edit target layer
|
||||
The `TransformCommand` SHALL store and restore the `SdfLayerHandle` that was the active edit target at the time of the edit. `Undo()` and `Redo()` SHALL direct their `UsdGeomXformCommonAPI` writes to that same layer via `UsdEditContext`.
|
||||
|
||||
#### Scenario: Undo writes to correct layer
|
||||
- **WHEN** the user has Layer B selected as the edit target, moves a prim, then undoes
|
||||
- **THEN** the revert is written to Layer B, not to the root layer
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
## 1. Core Command Infrastructure
|
||||
|
||||
- [x] 1.1 Create `src/core/CommandHistory.h` — define `ICommand` pure-virtual interface (`Execute`, `Undo`, `GetDescription`) and `CommandHistory` class with `Push`, `Undo`, `Redo`, `Clear`, `CanUndo`, `CanRedo`, `GetUndoDescription`, `GetRedoDescription`
|
||||
- [x] 1.2 Create `src/core/CommandHistory.cpp` — implement `CommandHistory` with undo/redo stacks (`std::vector<std::unique_ptr<ICommand>>`); `Push` executes command, pushes to undo stack, clears redo stack
|
||||
- [x] 1.3 Add `CommandHistory` as a member of `Application` and pass `CommandHistory*` into all panel and manager constructors/init methods
|
||||
- [x] 1.4 Call `m_commandHistory.Clear()` inside `Application::RefreshManagers()` so history resets on every stage lifecycle event
|
||||
- [x] 1.5 Add `CMakeLists.txt` entries for all new source files (`CommandHistory.cpp` and all command `.cpp` files added in subsequent tasks)
|
||||
|
||||
## 2. Transform Commands
|
||||
|
||||
- [x] 2.1 Create `src/core/commands/TransformCommand.h/.cpp` — stores prim `SdfPath`, `SdfLayerHandle` edit target, and pre/post TRS (`GfVec3d` translate, `GfVec3f` rotate, `GfVec3f` scale); `Execute()` applies post-values, `Undo()` applies pre-values via `UsdGeomXformCommonAPI` + `UsdEditContext`
|
||||
- [x] 2.2 Modify `TransformManipulator` — accept `CommandHistory*` in constructor/init; on drag-begin snapshot TRS into `m_dragStartTRS`; on drag-end (detect `wasDrawing && !m_isDragging`) push `TransformCommand` only when delta is non-zero
|
||||
- [x] 2.3 Modify `PropertyPanel` — accept `CommandHistory*`; capture pre-edit TRS before field edit begins; push `TransformCommand` inside `ImGui::IsItemDeactivatedAfterEdit()` blocks for translate, rotate, and scale fields
|
||||
|
||||
## 3. Scene Hierarchy Commands
|
||||
|
||||
- [x] 3.1 Create `src/core/commands/CreatePrimCommand.h/.cpp` — stores `SdfPath`, `TfToken typeName`, `SdfLayerHandle`; `Execute()` calls `stage->DefinePrim`; `Undo()` calls `stage->RemovePrim`
|
||||
- [x] 3.2 Create `src/core/commands/DeletePrimCommand.h/.cpp` — on construction serialises the target prim's spec via `SdfCopySpec` into an anonymous layer; `Execute()` removes the prim; `Undo()` restores from the anonymous layer via `SdfCopySpec`
|
||||
- [x] 3.3 Create `src/core/commands/AddReferenceCommand.h/.cpp` — stores created prim path and `SdfReference`; `Execute()` wraps in Xform and adds reference; `Undo()` removes the prim
|
||||
- [x] 3.4 Create `src/core/commands/ReplaceReferenceCommand.h/.cpp` — stores prim path, old `SdfReference`, new `SdfReference`; `Execute()` applies new reference; `Undo()` restores old
|
||||
- [x] 3.5 Modify `SceneHierarchyPanel` — accept `CommandHistory*`; replace direct `Application::CreatePrimOnStage` calls with `Push(CreatePrimCommand)`; replace direct delete with `Push(DeletePrimCommand)`; replace `AddReferenceToStage` with `Push(AddReferenceCommand)`; replace `ProcessPendingReplaceRef` with `Push(ReplaceReferenceCommand)`
|
||||
|
||||
## 4. Attribute Edit Commands
|
||||
|
||||
- [x] 4.1 Create `src/core/commands/AttributeSetCommand.h/.cpp` — stores `std::string description`, `std::function<void()> executeFunc`, `std::function<void()> undoFunc`; captures old value before set using `UsdAttribute::Get<T>` at the call site
|
||||
- [ ] 4.2 Modify `PropertyManager::SetPropertyValue` and `SetPropertyValueInLayer` — read old value, construct `AttributeSetCommand` with closures for old/new applies, push to `CommandHistory`
|
||||
|
||||
## 5. Layer Operation Commands
|
||||
|
||||
- [x] 5.1 Create `src/core/commands/LayerCreateCommand.h/.cpp` — stores layer file path and insertion index; `Execute()` calls `LayerManager::CreateSublayer`; `Undo()` calls `LayerManager::RemoveSublayer`
|
||||
- [x] 5.2 Create `src/core/commands/LayerRemoveCommand.h/.cpp` — stores layer path and original index; `Execute()` removes; `Undo()` re-inserts at saved index
|
||||
- [x] 5.3 Create `src/core/commands/LayerReorderCommand.h/.cpp` — stores full ordered-path list before and after; `Execute()` applies new order; `Undo()` restores old order by rewriting `rootLayer->GetSubLayerPaths()`
|
||||
- [x] 5.4 Modify `LayerPanel` — accept `CommandHistory*`; wrap Create, Remove, MoveUp, and MoveDown calls with the corresponding commands
|
||||
|
||||
## 6. Hotkeys and Menu Integration
|
||||
|
||||
- [x] 6.1 In `Application`'s main-loop (or `ImGuiContext`), detect `Ctrl+Z` and `Ctrl+Y` / `Ctrl+Shift+Z` key combinations when no ImGui text widget has input focus (`!ImGui::GetIO().WantTextInput`); call `m_commandHistory.Undo()` / `m_commandHistory.Redo()` accordingly
|
||||
- [x] 6.2 Add **Edit** menu (or extend existing menu bar) with **Undo** and **Redo** items; display `GetUndoDescription()` / `GetRedoDescription()` in item labels; disable items via `ImGui::BeginDisabled(!CanUndo())` / `(!CanRedo())`
|
||||
|
||||
## 7. Build and Verification
|
||||
|
||||
- [x] 7.1 Run `cmake --preset default` and `cmake --build build --config Release` — compilation succeeds; LNK1104 only occurs because `UsdLayerManager.exe` is currently running (expected)
|
||||
- [ ] 7.2 Run `cmake --install build --config Release` and launch `install/bin/App.exe`; manually verify: create a prim, undo removes it, redo restores it
|
||||
- [ ] 7.3 Manually verify gizmo drag undo: translate a prim with the Move gizmo, press Ctrl+Z, confirm prim returns to original position
|
||||
- [ ] 7.4 Run `ctest --test-dir build -C Release` and confirm all tests pass
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
## Context
|
||||
|
||||
The USD Layer Manager application currently has a viewport panel (`ViewportPanel`) driven by a built-in `ViewportCamera` class. The camera supports orbit, pan, zoom, and framing via bounding box. Input handling uses a direct LMB/MMB/scroll mapping without modifier keys. There is no concept of switching to a USD stage camera prim — the viewport always renders from the free camera's viewpoint.
|
||||
|
||||
The target users are Maya artists who expect Alt+button viewport navigation and the ability to view through authored cameras in the USD stage. The current input scheme is a friction point, and the inability to look through stage cameras limits creative preview workflows.
|
||||
|
||||
### Current State
|
||||
- `ViewportCamera` stores eye, focal point, orbit angles (yaw/pitch), distance, FOV, clip planes, aspect ratio
|
||||
- `ViewportPanel::HandleInput()` maps: LMB=orbit, MMB=pan, scroll=zoom, Shift+LMB=pan
|
||||
- `ViewportPanel::FrameScene()` frames on the entire stage bounds
|
||||
- No USD camera prim integration; no per-prim framing; no hotkey support
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Replace the viewport input mapping with Maya-style Alt+button navigation (Alt+LMB orbit, Alt+MMB pan, Alt+RMB dolly, scroll zoom)
|
||||
- Allow switching between the built-in free camera and any UsdCamera prim on the stage
|
||||
- Support framing the viewport on the selected prim's bounding box (F key) and the full stage (A key)
|
||||
- Add a camera selector dropdown in the viewport panel UI
|
||||
|
||||
**Non-Goals:**
|
||||
- Authoring or editing USD camera prims through the viewport (that belongs in the Property Panel)
|
||||
- Camera animation or sequencing (playblast, camera sequencer)
|
||||
- Multiple viewports or split-view layouts
|
||||
- Custom key binding configuration UI (hardcoded Maya-style for now)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Alt+button navigation with scroll exception
|
||||
|
||||
**Decision**: Use Alt+LMB for orbit, Alt+MMB for pan, Alt+RMB for dolly. Mouse scroll zooms without Alt (matching Maya behavior).
|
||||
|
||||
**Rationale**: This is the standard Maya viewport convention. Scroll zoom without Alt is expected because scroll has no other viewport conflict.
|
||||
|
||||
**Alternative considered**: Keep current LMB/MMB/scroll mapping as an option — rejected to avoid maintaining two input schemes and confusing users about the default.
|
||||
|
||||
### D2: Dual-mode ViewportCamera (free vs USD camera)
|
||||
|
||||
**Decision**: Extend `ViewportCamera` with an enum mode (`Free` / `UsdCamera`). In `UsdCamera` mode, the view/projection matrices are derived from the UsdCamera prim's transforms and attributes; the orbit/pan/zoom controls are disabled. In `Free` mode, behavior is unchanged.
|
||||
|
||||
**Rationale**: Keeping both modes in one class avoids duplicating the projection math and makes switching seamless. Disabling manual navigation when looking through a USD camera matches Maya's behavior (you navigate the camera's transform instead, which is out of scope for now).
|
||||
|
||||
**Alternative considered**: Separate `UsdCameraAdapter` class that wraps a UsdCamera into the same interface — more complex, no clear benefit until camera manipulation is needed.
|
||||
|
||||
### D3: Camera prim discovery and selection UI
|
||||
|
||||
**Decision**: Traverse the stage for all `UsdCamera`-typed prims on each frame (or on stage change). Present them in an ImGui combo dropdown above the viewport canvas. The first entry is always "Free Camera".
|
||||
|
||||
**Rationale**: Simple implementation; camera lists in typical USD scenes are small. Cache invalidation is handled by re-traversing on stage change notifications.
|
||||
|
||||
**Alternative considered**: Maintain a persistent camera registry — over-engineered for the current scope.
|
||||
|
||||
### D4: Frame selected via SceneHierarchyPanel selection
|
||||
|
||||
**Decision**: The `ViewportPanel` holds a callback/string for the selected prim path. `Application` wires the `SceneHierarchyPanel` selection callback to update this path. Pressing F computes the selected prim's bounding box and calls `FrameBoundingBox`. Pressing A calls the existing `FrameScene`.
|
||||
|
||||
**Rationale**: Minimal coupling; `ViewportPanel` doesn't need to know about `SceneHierarchyPanel` directly. The Application layer already owns both panels and can wire them.
|
||||
|
||||
### D5: Bounding box computation for selected prim
|
||||
|
||||
**Decision**: Use `UsdGeomBBoxCache` to compute the world-space bounding box of the selected prim and its descendants, then call `ViewportCamera::FrameBoundingBox`.
|
||||
|
||||
**Rationale**: This is the standard OpenUSD approach for bounding box queries and handles instancing, transforms, and purpose correctly.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Alt key conflicts with ImGui]** → ImGui may consume Alt for menu activation. Mitigation: Set `ImGuiConfigFlags_NoNav` or handle Alt detection via GLFW directly before ImGui processes it. If ImGui captures Alt, fall back to detecting Alt via `io.KeyAlt` and consuming the input in the viewport's `HandleInput()`.
|
||||
- **[Camera prim traversal cost on large stages]** → Re-traversing every frame is wasteful for stages with thousands of prims. Mitigation: Cache the camera list and invalidate on stage change only. For initial implementation, traversal per frame is acceptable; optimize later.
|
||||
- **[USD camera attribute coverage]** → Not all UsdCamera attributes (e.g., lens distortion, stereo) can be represented in the current ViewportCamera projection. Mitigation: Support core attributes (focalLength, horizontalAperture, clippingRange, projection type) and ignore unsupported attributes gracefully.
|
||||
- **[Free camera state lost on switch]** → When switching from USD camera back to free camera, the free camera's last position is restored. Mitigation: Store the free camera state separately and restore it on switch-back.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
## Why
|
||||
|
||||
The current viewport uses a fixed internal camera with basic orbit/pan/zoom controls that do not match industry-standard DCC conventions. Users cannot view the scene through USD stage cameras (e.g. perspective cameras authored in the USD file), and the input mapping differs from Maya's Alt+button convention, making the tool feel foreign to target users. Adding Maya-style controls and the ability to frame the selected prim are essential for a usable USD editing workflow.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add Maya-style viewport navigation: Alt+LMB to orbit, Alt+MMB to pan, Alt+RMB (or scroll) to dolly/zoom, with optional Alt-free scroll zoom
|
||||
- Add the ability to switch the viewport camera between the built-in free camera and any camera prim found on the USD stage
|
||||
- Add a "Frame Selected" action (press F key) that frames the viewport on the currently selected prim's bounding box
|
||||
- Add a "Frame All" action (press A key) that frames the viewport on the entire stage bounds
|
||||
- Add a camera selector dropdown in the viewport toolbar to switch between free camera and stage cameras
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `maya-viewport-controls`: Maya-style Alt+button navigation scheme replacing the current LMB/MMB/scroll mapping
|
||||
- `viewport-camera-switching`: Switch viewport rendering between the built-in free camera and USD stage camera prims
|
||||
- `viewport-frame-selection`: Frame viewport on selected prim (F key) and frame all (A key)
|
||||
|
||||
### Modified Capabilities
|
||||
- `imgui-docking`: No spec-level requirement changes; the viewport toolbar UI integrates within the existing dockable panel system
|
||||
|
||||
## Impact
|
||||
|
||||
- **ViewportCamera**: Add support for driving the view from an external USD camera prim (reading its transform and projection); refactor internal state to support both free-camera and USD-camera modes
|
||||
- **ViewportPanel**: Rewrite `HandleInput()` to use Maya-style Alt+button conventions; add camera selector UI in the viewport toolbar; add Frame Selected / Frame All hotkeys; expose camera switching
|
||||
- **UsdSceneRenderer**: No changes required — it already accepts view/projection matrices
|
||||
- **Application**: Wire up the selected prim path from SceneHierarchyPanel to the viewport for frame-selected functionality
|
||||
- **Dependencies**: No new third-party dependencies; all changes use existing OpenUSD and ImGui APIs
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Alt+LMB orbit navigation
|
||||
|
||||
The viewport SHALL orbit the camera around the focal point when the user holds Alt and drags with the left mouse button.
|
||||
|
||||
#### Scenario: Alt+LMB drag orbits the camera
|
||||
- **WHEN** the user holds Alt and drags with the left mouse button inside the viewport
|
||||
- **THEN** the camera orbits around the focal point based on the drag delta
|
||||
|
||||
#### Scenario: LMB without Alt does not orbit
|
||||
- **WHEN** the user drags with the left mouse button without holding Alt
|
||||
- **THEN** no orbit occurs and the click is forwarded for prim selection
|
||||
|
||||
### Requirement: Alt+MMB pan navigation
|
||||
|
||||
The viewport SHALL pan the camera when the user holds Alt and drags with the middle mouse button.
|
||||
|
||||
#### Scenario: Alt+MMB drag pans the camera
|
||||
- **WHEN** the user holds Alt and drags with the middle mouse button inside the viewport
|
||||
- **THEN** the camera pans horizontally and vertically based on the drag delta
|
||||
|
||||
### Requirement: Alt+RMB dolly navigation
|
||||
|
||||
The viewport SHALL dolly (zoom) the camera when the user holds Alt and drags with the right mouse button. Vertical drag upward moves the camera closer; downward moves it farther.
|
||||
|
||||
#### Scenario: Alt+RMB drag dollies the camera
|
||||
- **WHEN** the user holds Alt and drags vertically with the right mouse button inside the viewport
|
||||
- **THEN** the camera dollies in or out based on the vertical drag delta
|
||||
|
||||
### Requirement: Scroll zoom without Alt
|
||||
|
||||
The viewport SHALL zoom the camera when the user scrolls the mouse wheel, without requiring the Alt modifier.
|
||||
|
||||
#### Scenario: Scroll wheel zooms
|
||||
- **WHEN** the user scrolls the mouse wheel while hovering over the viewport
|
||||
- **THEN** the camera zooms in or out based on the scroll direction
|
||||
|
||||
### Requirement: Legacy input mappings removed
|
||||
|
||||
The previous input mappings (LMB for orbit, MMB for pan, Shift+LMB for pan) SHALL be replaced entirely by the Maya-style Alt+button scheme.
|
||||
|
||||
#### Scenario: MMB without Alt does not pan
|
||||
- **WHEN** the user drags with the middle mouse button without holding Alt
|
||||
- **THEN** no pan occurs
|
||||
|
||||
#### Scenario: Shift+LMB does not pan
|
||||
- **WHEN** the user holds Shift and drags with the left mouse button
|
||||
- **THEN** no pan occurs
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera selector dropdown in viewport
|
||||
|
||||
The viewport panel SHALL display a camera selector dropdown above the rendering canvas. The dropdown SHALL list "Free Camera" as the first entry, followed by all UsdCamera-typed prims found on the current USD stage.
|
||||
|
||||
#### Scenario: Dropdown shows free camera and stage cameras
|
||||
- **WHEN** a USD stage is loaded and contains camera prims at paths `/cameras/main` and `/cameras/top`
|
||||
- **THEN** the camera selector dropdown lists: "Free Camera", "/cameras/main", "/cameras/top"
|
||||
|
||||
#### Scenario: Dropdown shows only free camera with no stage cameras
|
||||
- **WHEN** a USD stage is loaded and contains no camera prims
|
||||
- **THEN** the camera selector dropdown lists only "Free Camera"
|
||||
|
||||
#### Scenario: No stage loaded
|
||||
- **WHEN** no USD stage is loaded
|
||||
- **THEN** the camera selector dropdown is disabled or shows only "Free Camera"
|
||||
|
||||
### Requirement: Switch to USD camera prim view
|
||||
|
||||
When the user selects a USD camera prim from the dropdown, the viewport SHALL render the scene from that camera's viewpoint by reading its transform and projection attributes.
|
||||
|
||||
#### Scenario: Select a USD camera from dropdown
|
||||
- **WHEN** the user selects "/cameras/main" from the camera dropdown
|
||||
- **THEN** the viewport renders the scene using the view matrix derived from that camera prim's world transform and the projection matrix derived from its focalLength, horizontalAperture, and clippingRange attributes
|
||||
|
||||
#### Scenario: USD camera attribute mapping
|
||||
- **WHEN** the viewport is rendering through a UsdCamera prim
|
||||
- **THEN** the projection matrix SHALL be computed from the camera's `focalLength`, `horizontalAperture`, `verticalAperture`, `clippingRange`, and `projection` attributes
|
||||
- **AND** the view matrix SHALL be computed from the camera prim's world-space transform
|
||||
|
||||
### Requirement: Free camera navigation disabled in USD camera mode
|
||||
|
||||
When the viewport is in USD camera mode, orbit, pan, dolly, and zoom controls SHALL be disabled.
|
||||
|
||||
#### Scenario: Attempt orbit while in USD camera mode
|
||||
- **WHEN** the viewport is viewing through a USD camera and the user performs Alt+LMB drag
|
||||
- **THEN** no orbit occurs and the viewport remains at the USD camera's viewpoint
|
||||
|
||||
### Requirement: Free camera state preserved across mode switches
|
||||
|
||||
The free camera's position, focal point, and orientation SHALL be preserved when switching to a USD camera and restored when switching back.
|
||||
|
||||
#### Scenario: Switch to USD camera and back
|
||||
- **WHEN** the user positions the free camera, switches to a USD camera, then switches back to "Free Camera"
|
||||
- **THEN** the viewport returns to the exact free camera position and orientation before the switch
|
||||
|
||||
### Requirement: Camera list refreshed on stage change
|
||||
|
||||
The list of available USD camera prims SHALL be refreshed when the stage changes (file open, close, or stage reload).
|
||||
|
||||
#### Scenario: Open a new stage with different cameras
|
||||
- **WHEN** the user opens a new USD file containing cameras at different paths
|
||||
- **THEN** the camera selector dropdown updates to reflect the new stage's camera prims
|
||||
|
||||
### Requirement: Default camera mode is free camera
|
||||
|
||||
The viewport SHALL start in free camera mode every time a stage is loaded.
|
||||
|
||||
#### Scenario: Stage loaded defaults to free camera
|
||||
- **WHEN** a USD stage is loaded
|
||||
- **THEN** the camera selector is set to "Free Camera" and the viewport renders from the built-in free camera
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Frame selected prim with F key
|
||||
|
||||
The viewport SHALL frame the camera on the bounding box of the currently selected prim when the user presses the F key while the viewport is focused.
|
||||
|
||||
#### Scenario: Frame a selected mesh prim
|
||||
- **WHEN** the user has a mesh prim selected in the scene hierarchy and presses F while the viewport is focused
|
||||
- **THEN** the camera repositions to frame the bounding box of that prim and all its descendants in world space
|
||||
|
||||
#### Scenario: Frame with no prim selected
|
||||
- **WHEN** the user presses F with no prim selected
|
||||
- **THEN** no camera movement occurs
|
||||
|
||||
#### Scenario: Frame in USD camera mode
|
||||
- **WHEN** the viewport is in USD camera mode and the user presses F
|
||||
- **THEN** the viewport switches to free camera mode and frames the selected prim
|
||||
|
||||
### Requirement: Frame all with A key
|
||||
|
||||
The viewport SHALL frame the camera on the bounding box of the entire USD stage when the user presses the A key while the viewport is focused.
|
||||
|
||||
#### Scenario: Frame all prims in the stage
|
||||
- **WHEN** the user presses A while the viewport is focused and a stage is loaded
|
||||
- **THEN** the camera repositions to frame the bounding box of the entire stage
|
||||
|
||||
#### Scenario: Frame all with no stage loaded
|
||||
- **WHEN** the user presses A with no stage loaded
|
||||
- **THEN** no camera movement occurs
|
||||
|
||||
### Requirement: Selected prim path sourced from SceneHierarchyPanel
|
||||
|
||||
The viewport SHALL obtain the currently selected prim path from the `SceneHierarchyPanel` via a callback wired by the `Application` class.
|
||||
|
||||
#### Scenario: Selection change triggers prim path update
|
||||
- **WHEN** the user selects a different prim in the SceneHierarchyPanel
|
||||
- **THEN** the viewport's stored selected prim path updates to reflect the new selection
|
||||
- **AND** pressing F frames the newly selected prim
|
||||
|
||||
### Requirement: Bounding box computed via UsdGeomBBoxCache
|
||||
|
||||
The bounding box for framing a selected prim SHALL be computed using `UsdGeomBBoxCache` to correctly account for transforms, instancing, and purpose.
|
||||
|
||||
#### Scenario: Bounding box includes descendant geometry
|
||||
- **WHEN** the user frames a prim that has mesh children
|
||||
- **THEN** the bounding box encompasses the prim and all its descendants' geometry in world space
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
## 1. Maya-style Viewport Controls
|
||||
|
||||
- [x] 1.1 Refactor `ViewportPanel::HandleInput()` to use Alt+LMB for orbit, Alt+MMB for pan, Alt+RMB for dolly
|
||||
- [x] 1.2 Remove legacy input mappings (LMB orbit, MMB pan, Shift+LMB pan) from `HandleInput()`
|
||||
- [x] 1.3 Keep mouse scroll zoom working without Alt modifier
|
||||
- [x] 1.4 Add Alt key detection via `io.KeyAlt` in ImGui and ensure Alt is not consumed by ImGui menu navigation
|
||||
|
||||
## 2. ViewportCamera Dual-Mode Support
|
||||
|
||||
- [x] 2.1 Add `enum class CameraMode { Free, UsdCamera }` and a `m_mode` member to `ViewportCamera`
|
||||
- [x] 2.2 Add `m_usdCameraPath` (SdfPath) and `m_savedFreeCameraState` struct to persist free camera state across mode switches
|
||||
- [x] 2.3 Add `SetUsdCamera(const UsdStageRefPtr& stage, const SdfPath& cameraPath)` method that reads UsdCamera attributes (focalLength, horizontalAperture, verticalAperture, clippingRange, projection) and world transform
|
||||
- [x] 2.4 Implement `ComputeUsdCameraViewMatrix()` deriving the view matrix from the UsdCamera prim's world-space transform
|
||||
- [x] 2.5 Implement `ComputeUsdCameraProjectionMatrix()` deriving the projection matrix from UsdCamera focalLength, aperture, and clip attributes
|
||||
- [x] 2.6 Modify `GetViewMatrix()` and `GetProjectionMatrix()` to dispatch based on `m_mode` (free vs USD camera)
|
||||
- [x] 2.7 Add `SwitchToFreeCamera()` that restores the saved free camera state
|
||||
- [x] 2.8 Disable orbit/pan/zoom/dolly calls when `m_mode == UsdCamera` (no-op in USD camera mode)
|
||||
|
||||
## 3. Camera Selector UI
|
||||
|
||||
- [x] 3.1 Add method `FindCameraPrims(const UsdStageRefPtr& stage)` to traverse the stage and return a vector of SdfPaths for all UsdCamera-typed prims
|
||||
- [x] 3.2 Cache the camera list in `ViewportPanel` and invalidate on stage change
|
||||
- [x] 3.3 Add an ImGui combo dropdown above the viewport canvas in `ViewportPanel::Render()` with "Free Camera" as the first entry followed by discovered camera prim paths
|
||||
- [x] 3.4 Wire the dropdown selection to call `ViewportCamera::SetUsdCamera()` or `SwitchToFreeCamera()`
|
||||
- [x] 3.5 Reset the camera selector to "Free Camera" when a new stage is loaded via `ViewportPanel::SetStage()`
|
||||
|
||||
## 4. Frame Selected Prim
|
||||
|
||||
- [x] 4.1 Add `m_selectedPrimPath` string member to `ViewportPanel` and a setter `SetSelectedPrimPath(const std::string&)`
|
||||
- [x] 4.2 Wire `Application` to call `ViewportPanel::SetSelectedPrimPath()` from the `SceneHierarchyPanel::SetOnPrimSelected()` callback
|
||||
- [x] 4.3 Add F key handler in `ViewportPanel::HandleInput()`: compute bounding box of the selected prim using `UsdGeomBBoxCache` and call `FrameBoundingBox`
|
||||
- [x] 4.4 If viewport is in USD camera mode when F is pressed, switch to free camera mode first then frame
|
||||
- [x] 4.5 Add A key handler in `ViewportPanel::HandleInput()`: call `FrameScene()` to frame all
|
||||
|
||||
## 5. Integration and Verification
|
||||
|
||||
- [x] 5.1 Verify Maya-style Alt+button navigation works for orbit, pan, and dolly in the viewport
|
||||
- [x] 5.2 Verify camera selector dropdown lists free camera and all stage camera prims
|
||||
- [x] 5.3 Verify switching to a USD camera renders from that camera's viewpoint
|
||||
- [x] 5.4 Verify switching back to free camera restores the previous free camera position
|
||||
- [x] 5.5 Verify F key frames the selected prim and A key frames the entire stage
|
||||
- [x] 5.6 Build and run the application to confirm no regressions in existing viewport rendering
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
## Context
|
||||
|
||||
The application is a USD Layer Manager built with ImGui (docking enabled) and OpenGL on Windows (WGL). The existing OpenGL context is initialized with `#version 130` (GLSL) and a legacy WGL pixel format (32-bit color, 24-bit depth, 8-bit stencil). The current "Viewport" panel in `Application::RenderUI()` is a placeholder displaying static text.
|
||||
|
||||
The OpenUSD v25.08 SDK is available with UsdGeom and UsdShade APIs for scene traversal. The CMake build system uses `GLOB_RECURSE` for source collection, so new `.cpp`/`.h` files under `src/` are automatically compiled.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Render USD mesh geometry in the existing Viewport ImGui panel using OpenGL
|
||||
- Provide interactive orbit/pan/zoom camera control via mouse input captured from the viewport region
|
||||
- Render a ground grid and axis gizmo for spatial orientation
|
||||
- Use FBO-based rendering so the 3D output composites cleanly with the ImGui docking layout
|
||||
- Support basic materials (diffuse color from UsdShade or primvar color) for visual differentiation
|
||||
|
||||
**Non-Goals:**
|
||||
- Hydra / HdRenderer integration (deferred — requires significant additional infrastructure)
|
||||
- Advanced shading (PBR, transparency, shadows, ambient occlusion)
|
||||
- Selection highlighting or picking (future change)
|
||||
- Animation playback
|
||||
- Multi-viewport or offscreen rendering to file
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. FBO-to-ImGui-Image rendering pipeline
|
||||
|
||||
**Decision**: Render the USD scene to an OpenGL Framebuffer Object, then display the attached color texture via `ImGui::Image()`.
|
||||
|
||||
**Rationale**: The ImGui docking system controls window layouts. Direct `glViewport` calls over the entire framebuffer would conflict with ImGui's rendering. FBO rendering produces a texture that ImGui can composite at the correct position and size. This is the standard pattern for 3D viewports in ImGui applications.
|
||||
|
||||
**Alternative considered**: Render directly to the default framebuffer with `glViewport` positioned over the viewport panel area. Rejected because it requires manual z-ordering with ImGui draw calls and breaks when docked windows overlap.
|
||||
|
||||
### 2. Direct UsdGeom traversal instead of Hydra
|
||||
|
||||
**Decision**: Traverse the UsdStage prim tree, extract `UsdGeomMesh` points/faceVertexCounts/faceVertexIndices/normals, and upload to OpenGL VBOs/VAOs for drawing.
|
||||
|
||||
**Rationale**: Hydra requires creating an `HdEngine`, `HdRenderDelegate`, and `HdRenderPass` infrastructure. For an initial viewport that draws meshes with basic color, direct traversal is simpler, has fewer dependencies, and gives us full control over the draw loop. Hydra can be integrated later as a modular swap.
|
||||
|
||||
**Alternative considered**: Use `UsdImagingGL` for immediate rendering. Rejected because UsdImagingGL is deprecated in favor of Hydra in recent USD versions and pulls in heavy imaging dependencies.
|
||||
|
||||
### 3. Camera model: Orbit camera with arcball control
|
||||
|
||||
**Decision**: Implement a turntable-style orbit camera. Left-drag orbits around a focal point, middle-drag pans, scroll-wheel zooms.
|
||||
|
||||
**Rationale**: Turntable cameras are the standard for DCC applications (Maya, Blender's default). Users familiar with 3D tools expect this interaction model. The camera stores: eye position, focal point, up vector, and computes a view matrix.
|
||||
|
||||
**Alternative considered**: Arcball camera (trackball rotation). More flexible but less predictable for typical CAD/DCC workflows. Can be added as an option later.
|
||||
|
||||
### 4. OpenGL GLSL #version 130 compatibility
|
||||
|
||||
**Decision**: Use GLSL 130 shaders with the existing OpenGL context. Use `gl_Vertex`/`gl_ModelViewProjectionMatrix`-style built-ins where needed, or explicit `in`/`out` with `#version 130` compatible syntax.
|
||||
|
||||
**Rationale**: The ImGui backend is initialized with `#version 130`. Creating a modern core-profile context would require changing the WGL initialization, potentially breaking ImGui rendering. Staying on the compatibility profile is lowest risk.
|
||||
|
||||
**Alternative considered**: Upgrade to OpenGL 3.3+ core profile. Rejected because it requires WGL context creation changes and may break existing ImGui OpenGL3 backend behavior.
|
||||
|
||||
### 5. Class decomposition
|
||||
|
||||
**Decision**: Create the following classes:
|
||||
- `ViewportPanel` (src/ui/) — owns the FBO, renderer, and camera; handles ImGui window and mouse input routing
|
||||
- `UsdSceneRenderer` (src/core/) — traverses USD stage, extracts geometry, manages OpenGL buffers, and issues draw calls
|
||||
- `ViewportCamera` (src/core/) — stores camera parameters, computes view/projection matrices, handles orbit/pan/zoom
|
||||
|
||||
**Rationale**: Separates concerns — rendering logic is independent of UI, camera logic is independent of both. Each class can be tested and developed in isolation.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Legacy OpenGL limitations]** → Stuck with compatibility profile until WGL context is upgraded. Mitigation: GLSL 130 is sufficient for basic mesh rendering; upgrade path is documented.
|
||||
- **[Large scene performance]** → Direct traversal loads all geometry into VBOs upfront. Mitigation: Add frustum culling and LOD in a future iteration; for now, limit to scenes that fit in GPU memory.
|
||||
- **[No Hydra = no procedural rendering]** → Complex procedurals (point instancers, curves) won't render correctly. Mitigation: Document supported prim types; add Hydra path later.
|
||||
- **[FBO resize on dock change]** → Viewport panel size changes when docked/undocked. Mitigation: `ViewportPanel` queries `ImGui::GetContentRegionAvail()` each frame and resizes the FBO when dimensions change.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## Why
|
||||
|
||||
The application currently renders a placeholder "3D viewport will be displayed here" text instead of showing actual USD scene geometry. Users cannot visually inspect their USD stages, making the tool incomplete for scene authoring workflows. A functional viewport is essential for any USD editing application — without it, users must switch to an external viewer (e.g., usdview) to see their changes.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add an OpenGL-based 3D viewport that renders USD stage geometry within the existing dockable "Viewport" panel
|
||||
- Implement a camera system (orbit, pan, zoom) for navigating the 3D scene
|
||||
- Add Hydra or direct UsdGeom-based rendering to traverse the stage and draw meshes, transforms, and materials
|
||||
- Provide viewport controls (grid display, axis indicator, background color) via a toolbar or context menu
|
||||
- Render a ground grid and axis gizmo for spatial orientation
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `viewport-renderer`: OpenGL rendering pipeline that traverses USD prims and draws geometry, materials, and transforms into an FBO-backed ImGui image
|
||||
- `viewport-camera`: Interactive camera controller supporting orbit, pan, and zoom with mouse input mapped through the ImGui viewport window
|
||||
- `viewport-overlay`: Grid, axis gizmo, and viewport settings overlay rendered on top of the 3D scene
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
## Impact
|
||||
|
||||
- **src/ui/Application.h/cpp**: Replace placeholder viewport section with a ViewportPanel that owns the renderer and camera
|
||||
- **New files**: ViewportPanel, UsdSceneRenderer, ViewportCamera classes
|
||||
- **Dependencies**: OpenGL 3.3+ (already available via WGL context), OpenUSD UsdGeom/UsdShade APIs
|
||||
- **Build system**: CMakeLists.txt must compile new source files and link OpenGL
|
||||
- **Rendering path**: Initially UsdGeom traversal with OpenGL draw calls; Hydra integration deferred to a future change
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera supports orbit, pan, and zoom interaction
|
||||
|
||||
The system SHALL provide a turntable-style orbit camera controlled by mouse input within the Viewport panel. Left-drag orbits around a focal point, middle-drag or shift+left-drag pans, and scroll-wheel zooms.
|
||||
|
||||
#### Scenario: Orbit the camera
|
||||
- **WHEN** the user holds the left mouse button and drags within the Viewport panel
|
||||
- **THEN** the camera orbits around the focal point
|
||||
- **AND** the view updates in real-time
|
||||
|
||||
#### Scenario: Pan the camera
|
||||
- **WHEN** the user holds the middle mouse button (or shift+left button) and drags within the Viewport panel
|
||||
- **THEN** the camera pans horizontally and vertically relative to the view plane
|
||||
- **AND** the focal point moves with the camera
|
||||
|
||||
#### Scenario: Zoom the camera
|
||||
- **WHEN** the user scrolls the mouse wheel within the Viewport panel
|
||||
- **THEN** the camera moves closer to or farther from the focal point
|
||||
- **AND** the zoom is proportional to the current distance from the focal point
|
||||
|
||||
### Requirement: Camera computes view and projection matrices
|
||||
|
||||
The system SHALL compute a view matrix from the camera's eye position, focal point, and up vector, and a perspective projection matrix from the field of view, aspect ratio, and near/far clip planes.
|
||||
|
||||
#### Scenario: View matrix from camera parameters
|
||||
- **WHEN** the camera parameters (eye, focal point, up) are set or updated
|
||||
- **THEN** the system computes a `lookAt` view matrix
|
||||
|
||||
#### Scenario: Projection matrix matches viewport aspect ratio
|
||||
- **WHEN** the FBO dimensions change
|
||||
- **THEN** the projection matrix is recomputed with the updated aspect ratio
|
||||
|
||||
### Requirement: Mouse input is captured only when the viewport is hovered
|
||||
|
||||
The system SHALL capture mouse input for camera control only when the ImGui Viewport window is hovered. Mouse events outside the viewport SHALL NOT affect the camera.
|
||||
|
||||
#### Scenario: Mouse input captured in viewport
|
||||
- **WHEN** the mouse cursor is over the Viewport panel and the user interacts
|
||||
- **THEN** the camera responds to the input
|
||||
|
||||
#### Scenario: Mouse input ignored outside viewport
|
||||
- **WHEN** the mouse cursor is outside the Viewport panel
|
||||
- **THEN** camera input is not processed
|
||||
|
||||
### Requirement: Camera frames the scene automatically
|
||||
|
||||
The system SHALL compute a default camera position that frames the bounding box of the scene when a stage is first loaded.
|
||||
|
||||
#### Scenario: Camera frames scene on stage load
|
||||
- **WHEN** a new USD stage is opened
|
||||
- **THEN** the camera position and focal point are set so the entire scene bounding box is visible
|
||||
- **AND** the camera distance is adjusted based on the scene size
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Ground grid is rendered in the viewport
|
||||
|
||||
The system SHALL render a ground plane grid at the Y=0 plane (XZ plane) to provide spatial reference.
|
||||
|
||||
#### Scenario: Grid visible by default
|
||||
- **WHEN** the viewport is displaying a scene
|
||||
- **THEN** a grid is drawn on the Y=0 plane with lines at regular intervals
|
||||
|
||||
#### Scenario: Grid can be toggled off
|
||||
- **WHEN** the user toggles the grid display off via the viewport context menu
|
||||
- **THEN** the grid is no longer rendered
|
||||
|
||||
### Requirement: Axis gizmo is rendered in the viewport corner
|
||||
|
||||
The system SHALL render a 3D axis indicator (RGB for XYZ) in the lower-left corner of the viewport to show scene orientation.
|
||||
|
||||
#### Scenario: Axis gizmo visible
|
||||
- **WHEN** the viewport is displaying a scene
|
||||
- **THEN** an axis gizmo is drawn in the lower-left corner with X=red, Y=green, Z=blue lines and labels
|
||||
|
||||
#### Scenario: Axis gizmo reflects camera orientation
|
||||
- **WHEN** the camera is rotated
|
||||
- **THEN** the axis gizmo orientation updates to reflect the current view direction
|
||||
|
||||
### Requirement: Viewport background color is configurable
|
||||
|
||||
The system SHALL render the viewport background with a configurable color, defaulting to a dark gray (0.15, 0.15, 0.15).
|
||||
|
||||
#### Scenario: Default background color
|
||||
- **WHEN** the viewport is rendered
|
||||
- **THEN** the background is cleared to dark gray (0.15, 0.15, 0.15)
|
||||
|
||||
#### Scenario: Background color changed via context menu
|
||||
- **WHEN** the user selects a different background color from the viewport settings
|
||||
- **THEN** the viewport background is updated to the selected color
|
||||
|
||||
### Requirement: Viewport context menu provides display options
|
||||
|
||||
The system SHALL provide a right-click context menu in the viewport with toggles for grid display and background color options.
|
||||
|
||||
#### Scenario: Right-click opens context menu
|
||||
- **WHEN** the user right-clicks within the viewport area
|
||||
- **THEN** a context menu appears with options: "Show Grid" (toggle), and "Background" submenu with color presets
|
||||
|
||||
#### Scenario: Toggle grid from context menu
|
||||
- **WHEN** the user clicks "Show Grid" in the context menu
|
||||
- **THEN** the grid display is toggled on or off
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Viewport renders USD stage geometry into an FBO-backed ImGui panel
|
||||
|
||||
The system SHALL render USD stage geometry into an OpenGL Framebuffer Object (FBO) and display the attached color texture via `ImGui::Image()` within the "Viewport" dockable panel.
|
||||
|
||||
#### Scenario: Viewport displays scene geometry when a stage is loaded
|
||||
- **WHEN** a USD stage is opened and the Viewport panel is visible
|
||||
- **THEN** the system traverses the stage's prim tree, extracts `UsdGeomMesh` data, and renders mesh geometry into the FBO
|
||||
- **AND** the FBO color texture is displayed as an ImGui image in the Viewport panel
|
||||
|
||||
#### Scenario: Viewport shows empty background when no stage is loaded
|
||||
- **WHEN** no USD stage is open
|
||||
- **THEN** the Viewport panel displays a dark background with no geometry
|
||||
|
||||
### Requirement: FBO resizes to match viewport panel dimensions
|
||||
|
||||
The system SHALL resize the FBO color and depth attachments whenever the Viewport panel's content region dimensions change.
|
||||
|
||||
#### Scenario: FBO resized on dock layout change
|
||||
- **WHEN** the user docks, undocks, or resizes the Viewport panel
|
||||
- **THEN** the FBO is recreated with the new width and height from `ImGui::GetContentRegionAvail()`
|
||||
- **AND** the previous FBO textures are deleted
|
||||
|
||||
#### Scenario: No unnecessary FBO reallocation
|
||||
- **WHEN** the Viewport panel dimensions remain unchanged between frames
|
||||
- **THEN** the FBO is not recreated
|
||||
|
||||
### Requirement: USD mesh geometry is extracted and uploaded to GPU buffers
|
||||
|
||||
The system SHALL traverse the UsdStage, extract `UsdGeomMesh` points, face vertex counts, face vertex indices, and normals, and upload them to OpenGL VBOs/VAOs.
|
||||
|
||||
#### Scenario: Mesh data loaded from UsdGeomMesh prims
|
||||
- **WHEN** the stage is loaded or refreshed
|
||||
- **THEN** for each `UsdGeomMesh` prim, the system reads `points`, `faceVertexCounts`, `faceVertexIndices`, and `normals` attributes
|
||||
- **AND** uploads the data to OpenGL vertex buffer objects
|
||||
|
||||
#### Scenario: Missing normals handled gracefully
|
||||
- **WHEN** a `UsdGeomMesh` prim has no authored normals
|
||||
- **THEN** the system computes flat face normals from the triangle winding
|
||||
- **AND** renders the mesh with the computed normals
|
||||
|
||||
### Requirement: Meshes are rendered with diffuse color from UsdShade or primvar
|
||||
|
||||
The system SHALL apply a diffuse color to each mesh obtained from `UsdShadeMaterial` binding or a `displayColor` primvar, falling back to a default color.
|
||||
|
||||
#### Scenario: Mesh with UsdShade material
|
||||
- **WHEN** a mesh prim has a bound `UsdShadeMaterial` with a diffuse color output
|
||||
- **THEN** the mesh is rendered with that diffuse color
|
||||
|
||||
#### Scenario: Mesh with displayColor primvar
|
||||
- **WHEN** a mesh prim has a `primvars:displayColor` attribute and no bound material
|
||||
- **THEN** the mesh is rendered with the displayColor value
|
||||
|
||||
#### Scenario: Mesh with no color information
|
||||
- **WHEN** a mesh prim has neither a bound material nor a displayColor primvar
|
||||
- **THEN** the mesh is rendered with a default gray color (0.7, 0.7, 0.7)
|
||||
|
||||
### Requirement: OpenGL shaders are GLSL #version 130 compatible
|
||||
|
||||
All viewport shaders SHALL be written in GLSL #version 130 and use the compatibility profile built-in variables (`gl_Vertex`, attribute bindings, etc.) to match the existing OpenGL context.
|
||||
|
||||
#### Scenario: Shaders compile on legacy OpenGL context
|
||||
- **WHEN** the application initializes the viewport renderer
|
||||
- **THEN** the vertex and fragment shaders compile successfully on the existing WGL OpenGL context initialized with `#version 130`
|
||||
|
||||
### Requirement: Scene rendering is refreshed when the stage changes
|
||||
|
||||
The system SHALL rebuild GPU geometry buffers when the USD stage is opened, closed, or when the stage content is modified.
|
||||
|
||||
#### Scenario: Stage opened triggers rebuild
|
||||
- **WHEN** a new USD stage is opened
|
||||
- **THEN** the renderer clears existing GPU buffers and re-traverses the new stage
|
||||
|
||||
#### Scenario: Stage closed clears geometry
|
||||
- **WHEN** the current stage is closed
|
||||
- **THEN** the renderer clears all GPU buffers and the viewport shows an empty scene
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
## 1. ViewportCamera
|
||||
|
||||
- [x] 1.1 Create `src/core/ViewportCamera.h` with class declaration: eye, focalPoint, up vector, fov, nearClip, farClip, aspectRatio; methods for orbit/pan/zoom, view matrix, projection matrix computation
|
||||
- [x] 1.2 Implement `src/core/ViewportCamera.cpp`: `LookAt` view matrix, perspective projection matrix, orbit (spherical coords around focal point), pan (translate eye+focal in view plane), zoom (adjust distance to focal point proportionally)
|
||||
- [x] 1.3 Add `FrameBoundingBox(bounding box)` method that positions the camera to view the entire scene
|
||||
|
||||
## 2. UsdSceneRenderer
|
||||
|
||||
- [x] 2.1 Create `src/core/UsdSceneRenderer.h` with class declaration: stage ref, OpenGL buffer handles, methods for `SetStage()`, `Rebuild()`, `Render()`, `Cleanup()`
|
||||
- [x] 2.2 Implement `src/core/UsdSceneRenderer.cpp` — stage traversal: iterate prims, filter `UsdGeomMesh`, extract `points`, `faceVertexCounts`, `faceVertexIndices`, `normals`
|
||||
- [x] 2.3 Implement geometry upload: create VAO/VBO per mesh, handle triangulation from `faceVertexCounts`, compute flat normals when authored normals are missing
|
||||
- [x] 2.4 Implement material color extraction: read `UsdShadeMaterial` binding diffuse color, fall back to `primvars:displayColor`, fall back to default gray (0.7, 0.7, 0.7)
|
||||
- [x] 2.5 Implement GLSL #version 130 shaders: vertex shader (MVP transform + normal transform), fragment shader (directional light + ambient + diffuse color)
|
||||
- [x] 2.6 Implement `Render()` method: bind shader, set view/projection uniforms, iterate meshes and draw with their material colors
|
||||
- [x] 2.7 Implement `Cleanup()` and `Rebuild()`: delete old OpenGL buffers, re-traverse stage, re-upload geometry
|
||||
|
||||
## 3. ViewportPanel (FBO + ImGui Integration)
|
||||
|
||||
- [x] 3.1 Create `src/ui/ViewportPanel.h` with class declaration: FBO/color/depth texture handles, `ViewportCamera`, `UsdSceneRenderer`, dimensions; methods for `SetStage()`, `Render()`, `HandleInput()`
|
||||
- [x] 3.2 Implement FBO creation and resize logic: create color + depth attachments, detect dimension changes from `ImGui::GetContentRegionAvail()`, resize FBO only when dimensions change
|
||||
- [x] 3.3 Implement `Render()`: bind FBO, clear with background color, call `UsdSceneRenderer::Render()` with camera matrices, unbind FBO, display color texture via `ImGui::Image()`
|
||||
- [x] 3.4 Implement mouse input routing: detect viewport hover via `ImGui::IsWindowHovered()`, capture left-drag (orbit), middle-drag/shift+left-drag (pan), scroll (zoom), forward deltas to `ViewportCamera`
|
||||
|
||||
## 4. Viewport Overlay (Grid + Gizmo + Context Menu)
|
||||
|
||||
- [x] 4.1 Implement ground grid rendering: draw lines on Y=0 plane at regular intervals using GL_LINES in the scene renderer pass
|
||||
- [x] 4.2 Implement axis gizmo rendering: draw RGB (XYZ) lines in the lower-left viewport corner using an orthographic overlay pass independent of the scene camera
|
||||
- [x] 4.3 Implement background color configuration: store background color, use it for `glClear` in the FBO render pass
|
||||
- [x] 4.4 Implement viewport right-click context menu with "Show Grid" toggle and "Background" color preset submenu
|
||||
|
||||
## 5. Application Integration
|
||||
|
||||
- [x] 5.1 Add `ViewportPanel` member to `Application`, initialize in `Application::Initialize()`, wire to `UsdStageManager` via `SetStage()`
|
||||
- [x] 5.2 Replace the placeholder Viewport `ImGui::Begin/End` block in `Application::RenderUI()` with `ViewportPanel::Render()`
|
||||
- [x] 5.3 Call `ViewportCamera::FrameBoundingBox()` in `RefreshManagers()` when a new stage is loaded so the camera frames the scene
|
||||
- [x] 5.4 Verify all existing panels (Scene Hierarchy, Layer Panel, Property Panel) still render correctly alongside the new ViewportPanel
|
||||
|
||||
## 6. Build and Test
|
||||
|
||||
- [x] 6.1 Run `cmake --preset default` and `cmake --build` to verify compilation with new source files
|
||||
- [x] 6.2 Launch the application, open a USD file with mesh geometry, verify geometry renders in the Viewport panel
|
||||
- [x] 6.3 Test orbit, pan, zoom interactions in the viewport
|
||||
- [x] 6.4 Test grid toggle and background color from context menu
|
||||
- [x] 6.5 Test FBO resize by docking/undocking/resizing the Viewport panel
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Docking is enabled at compile time
|
||||
|
||||
The build system SHALL define `IMGUI_HAS_DOCK` prior to compiling any translation unit that includes `imgui.h`.
|
||||
|
||||
#### Scenario: ImGui compiled with docking support
|
||||
- **WHEN** the project is compiled with CMake
|
||||
- **THEN** `IMGUI_HAS_DOCK` is defined for all imgui source files and any file including `imgui.h`
|
||||
|
||||
### Requirement: Docking is enabled at runtime
|
||||
|
||||
The application SHALL set `ImGuiConfigFlags_DockingEnable` in the ImGui IO configuration during initialization.
|
||||
|
||||
#### Scenario: Docking flag set on startup
|
||||
- **WHEN** the application starts and `ImGuiContext::Initialize()` is called
|
||||
- **THEN** `io.ConfigFlags` includes `ImGuiConfigFlags_DockingEnable`
|
||||
|
||||
### Requirement: Main dockspace spans the application window
|
||||
|
||||
The application SHALL create a full-viewport dockspace via `ImGui::DockSpaceOverViewport()` at the start of each frame's UI rendering.
|
||||
|
||||
#### Scenario: Dockspace created each frame
|
||||
- **WHEN** `Application::RenderUI()` executes
|
||||
- **THEN** `ImGui::DockSpaceOverViewport()` is called after `NewFrame()` and before any panel `Begin/End` calls
|
||||
|
||||
#### Scenario: Dockspace fills the entire viewport
|
||||
- **WHEN** the application window is resized
|
||||
- **THEN** the dockspace automatically fills the new window dimensions
|
||||
|
||||
### Requirement: All panels are dockable
|
||||
|
||||
All application panels SHALL be created as dockable ImGui windows that integrate with the dockspace.
|
||||
|
||||
#### Scenario: Panel windows use standard Begin/End
|
||||
- **WHEN** any panel window is rendered
|
||||
- **THEN** its `ImGui::Begin()` call does not include `SetNextWindowPos` or `SetNextWindowSize` calls
|
||||
- **AND** the window can be dragged, docked, tabbed, or floated by the user
|
||||
|
||||
#### Scenario: Core panels cannot be collapsed
|
||||
- **WHEN** the Scene Hierarchy, Layer Panel, Viewport, or Property Panel windows are rendered
|
||||
- **THEN** each window is created with `ImGuiWindowFlags_NoCollapse`
|
||||
|
||||
### Requirement: Window visibility toggles persist
|
||||
|
||||
Existing View menu toggles for Stage Info and Demo Window SHALL continue to control window visibility.
|
||||
|
||||
#### Scenario: Toggle Stage Info visibility
|
||||
- **WHEN** the user clicks "Stage Info" in the View menu
|
||||
- **THEN** the Stage Info window appears or disappears
|
||||
- **AND** the toggle state is reflected by the checkmark in the menu item
|
||||
|
||||
#### Scenario: Toggle Demo Window visibility
|
||||
- **WHEN** the user clicks "ImGui Demo" in the View menu
|
||||
- **THEN** the ImGui Demo Window appears or disappears
|
||||
- **AND** the toggle state is reflected by the checkmark in the menu item
|
||||
|
||||
### Requirement: Dock layout persists across sessions
|
||||
|
||||
The docking layout SHALL be saved to and restored from `imgui.ini` automatically by ImGui's built-in persistence.
|
||||
|
||||
#### Scenario: Layout restored on restart
|
||||
- **WHEN** the user arranges panels into a custom docking layout and restarts the application
|
||||
- **THEN** the previous docking layout is restored
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Pixel aliasing icon: lower-left triangle of square "pixels" with gaps,
|
||||
showing the classic staircase / aliased-diagonal pattern -->
|
||||
<path d="M 14 14 L 114 14 L 114 114 L 14 114 Z
|
||||
M 14 142 L 114 142 L 114 242 L 14 242 Z
|
||||
M 142 142 L 242 142 L 242 242 L 142 242 Z
|
||||
M 14 270 L 114 270 L 114 370 L 14 370 Z
|
||||
M 142 270 L 242 270 L 242 370 L 142 370 Z
|
||||
M 270 270 L 370 270 L 370 370 L 270 370 Z
|
||||
M 14 398 L 114 398 L 114 498 L 14 498 Z
|
||||
M 142 398 L 242 398 L 242 498 L 142 498 Z
|
||||
M 270 398 L 370 398 L 370 498 L 270 498 Z
|
||||
M 398 398 L 498 398 L 498 498 L 398 498 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L240 103l0 137-137 0 15.3-15.3c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L103 272l137 0 0 137-15.3-15.3c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L272 409l0-137 137 0-15.3 15.3c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L409 240l-137 0 0-137 15.3 15.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-64-64z"/></svg>
|
||||
|
After Width: | Height: | Size: 946 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160 352 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l111.5 0c0 0 0 0 0 0l.4 0c17.7 0 32-14.3 32-32l0-112c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 35.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5zM39 289.3c-5 1.5-9.8 4.2-13.7 8.2c-4 4-6.7 8.8-8.1 14c-.3 1.2-.6 2.5-.8 3.8c-.3 1.7-.4 3.4-.4 5.1L16 432c0 17.7 14.3 32 32 32s32-14.3 32-32l0-35.1 17.6 17.5c87.5 87.4 229.3 87.4 316.7 0c24.4-24.4 42.1-53.1 52.9-83.7c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.5 62.5-163.8 62.5-226.3 0l-17.1-17.1 45.2 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L48 288c-3 0-5.9 .4-8.6 1.1l-.4 .2z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M344 0H488c13.3 0 24 10.7 24 24V168c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-39-39-87 87c-9.4 9.4-24.6 9.4-33.9 0l-32-32c-9.4-9.4-9.4-24.6 0-33.9l87-87L327 41c-6.9-6.9-8.9-17.2-5.2-26.2S334.3 0 344 0zM168 512H24c-13.3 0-24-10.7-24-24V344c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2l39 39 87-87c9.4-9.4 24.6-9.4 33.9 0l32 32c9.4 9.4 9.4 24.6 0 33.9l-87 87 39 39c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 703 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M149.1 64.8L138.7 96 64 96C28.7 96 0 124.7 0 160L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64l-74.7 0L362.9 64.8C356.4 45.2 338.1 32 317.4 32L194.6 32c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
|
||||
|
After Width: | Height: | Size: 549 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-352a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
|
||||
|
After Width: | Height: | Size: 383 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512z"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 753 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M234.5 5.7c13.9-5 29.1-5 43.1 0l192 68.6C495 83.4 512 107.5 512 134.6l0 242.9c0 27-17 51.2-42.5 60.3l-192 68.6c-13.9 5-29.1 5-43.1 0l-192-68.6C17 428.6 0 404.5 0 377.4L0 134.6c0-27 17-51.2 42.5-60.3l192-68.6zM256 66L82.3 128 256 190l173.7-62L256 66zm32 368.6l160-57.1 0-188L288 246.6l0 188z"/></svg>
|
||||
|
After Width: | Height: | Size: 581 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Cursor arrow redrawn in a square viewBox so NanoSVG does not crop it.
|
||||
Tip at upper-left; body runs down then sweeps right for the tail. -->
|
||||
<path d="M 80 40 L 80 390 L 190 275 L 248 432 L 322 398 L 265 242 L 432 242 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 863 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"/></svg>
|
||||
|
After Width: | Height: | Size: 653 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Grid toggle: 3x3 hashtag grid lines --><path d="M 64 108 L 448 108 L 448 140 L 64 140 Z M 64 240 L 448 240 L 448 272 L 64 272 Z M 64 372 L 448 372 L 448 404 L 64 404 Z M 108 64 L 140 64 L 140 448 L 108 448 Z M 240 64 L 272 64 L 272 448 L 240 448 Z M 372 64 L 404 64 L 404 448 L 372 448 Z"/></svg>
|
||||
|
After Width: | Height: | Size: 364 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M264.5 5.2c14.9-6.9 32.1-6.9 47 0l218.6 101c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 149.8C37.4 145.8 32 137.3 32 128s5.4-17.9 13.9-21.8L264.5 5.2zM476.9 209.6l53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 277.8C37.4 273.8 32 265.3 32 256s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0l152-70.2zm-152 198.2l152-70.2 53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 405.8C37.4 401.8 32 393.3 32 384s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 920 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="9" height="20" rx="2" fill="white"/>
|
||||
<rect x="13" y="2" width="9" height="20" rx="2" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 197 B |
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="9" height="9" rx="2" fill="white"/>
|
||||
<rect x="13" y="2" width="9" height="9" rx="2" fill="white"/>
|
||||
<rect x="2" y="13" width="9" height="9" rx="2" fill="white"/>
|
||||
<rect x="13" y="13" width="9" height="9" rx="2" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 328 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="20" height="20" rx="2" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 133 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="20" height="9" rx="2" fill="white"/>
|
||||
<rect x="2" y="13" width="20" height="9" rx="2" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 197 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
|
||||
|
After Width: | Height: | Size: 726 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M234.5 5.7c13.9-5 29.1-5 43.1 0l192 68.6C495 83.4 512 107.5 512 134.6l0 242.9c0 27-17 51.2-42.5 60.3l-192 68.6c-13.9 5-29.1 5-43.1 0l-192-68.6C17 428.6 0 404.5 0 377.4L0 134.6c0-27 17-51.2 42.5-60.3l192-68.6zM256 66L82.3 128 256 190l173.7-62L256 66zm32 368.6l160-57.1 0-188L288 246.6l0 188z"/></svg>
|
||||
|
After Width: | Height: | Size: 582 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M32 119.4C12.9 108.4 0 87.7 0 64C0 28.7 28.7 0 64 0c23.7 0 44.4 12.9 55.4 32l337.1 0C467.6 12.9 488.3 0 512 0c35.3 0 64 28.7 64 64c0 23.7-12.9 44.4-32 55.4l0 273.1c19.1 11.1 32 31.7 32 55.4c0 35.3-28.7 64-64 64c-23.7 0-44.4-12.9-55.4-32l-337.1 0c-11.1 19.1-31.7 32-55.4 32c-35.3 0-64-28.7-64-64c0-23.7 12.9-44.4 32-55.4l0-273.1zM456.6 96L119.4 96c-5.6 9.7-13.7 17.8-23.4 23.4l0 273.1c9.7 5.6 17.8 13.7 23.4 23.4l337.1 0c5.6-9.7 13.7-17.8 23.4-23.4l0-273.1c-9.7-5.6-17.8-13.7-23.4-23.4zM128 160c0-17.7 14.3-32 32-32l128 0c17.7 0 32 14.3 32 32l0 96c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-96zM256 320l32 0c35.3 0 64-28.7 64-64l0-32 64 0c17.7 0 32 14.3 32 32l0 96c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-32z"/></svg>
|
||||
|
After Width: | Height: | Size: 1019 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 32C0 14.3 14.3 0 32 0L160 0c17.7 0 32 14.3 32 32l0 384c0 53-43 96-96 96s-96-43-96-96L0 32zM223.6 425.9c.3-3.3 .4-6.6 .4-9.9l0-262 75.4-75.4c12.5-12.5 32.8-12.5 45.3 0l90.5 90.5c12.5 12.5 12.5 32.8 0 45.3L223.6 425.9zM182.8 512l192-192L480 320c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-297.2 0zM128 64L64 64l0 64 64 0 0-64zM64 192l0 64 64 0 0-64-64 0zM96 440a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
|
||||
|
After Width: | Height: | Size: 693 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,59 @@
|
|||
#include "CommandHistory.h"
|
||||
#include "../utils/Logger.h"
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
void CommandHistory::Push(std::unique_ptr<ICommand> cmd) {
|
||||
if (!cmd) return;
|
||||
try {
|
||||
cmd->Execute();
|
||||
} catch (...) {
|
||||
LOG_ERROR("CommandHistory::Push — Execute() threw an exception; command discarded");
|
||||
return;
|
||||
}
|
||||
m_undoStack.push_back(std::move(cmd));
|
||||
m_redoStack.clear();
|
||||
}
|
||||
|
||||
void CommandHistory::Undo() {
|
||||
if (m_undoStack.empty()) return;
|
||||
auto& cmd = m_undoStack.back();
|
||||
try {
|
||||
cmd->Undo();
|
||||
} catch (...) {
|
||||
LOG_ERROR("CommandHistory::Undo — Undo() threw an exception; history cleared for safety");
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
m_redoStack.push_back(std::move(cmd));
|
||||
m_undoStack.pop_back();
|
||||
}
|
||||
|
||||
void CommandHistory::Redo() {
|
||||
if (m_redoStack.empty()) return;
|
||||
auto& cmd = m_redoStack.back();
|
||||
try {
|
||||
cmd->Execute();
|
||||
} catch (...) {
|
||||
LOG_ERROR("CommandHistory::Redo — Execute() threw an exception; history cleared for safety");
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
m_undoStack.push_back(std::move(cmd));
|
||||
m_redoStack.pop_back();
|
||||
}
|
||||
|
||||
void CommandHistory::Clear() {
|
||||
m_undoStack.clear();
|
||||
m_redoStack.clear();
|
||||
}
|
||||
|
||||
std::string CommandHistory::GetUndoDescription() const {
|
||||
return m_undoStack.empty() ? std::string() : m_undoStack.back()->GetDescription();
|
||||
}
|
||||
|
||||
std::string CommandHistory::GetRedoDescription() const {
|
||||
return m_redoStack.empty() ? std::string() : m_redoStack.back()->GetDescription();
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Pure-virtual interface for all reversible operations.
|
||||
class ICommand {
|
||||
public:
|
||||
virtual ~ICommand() = default;
|
||||
virtual void Execute() = 0;
|
||||
virtual void Undo() = 0;
|
||||
virtual std::string GetDescription() const = 0;
|
||||
};
|
||||
|
||||
/// Application-level undo/redo stack.
|
||||
///
|
||||
/// Push(cmd) executes the command, pushes it onto the undo stack,
|
||||
/// and clears the redo stack. Undo() pops from the undo stack, calls
|
||||
/// Undo() on the command, and pushes it onto the redo stack (and vice
|
||||
/// versa for Redo()).
|
||||
///
|
||||
/// Call Clear() whenever the USD stage is replaced so stale USD object
|
||||
/// references (UsdPrim, UsdAttribute, SdfLayerHandle) cannot be dereferenced.
|
||||
class CommandHistory {
|
||||
public:
|
||||
CommandHistory() = default;
|
||||
~CommandHistory() = default;
|
||||
|
||||
/// Execute cmd and push onto undo stack; clears redo stack.
|
||||
void Push(std::unique_ptr<ICommand> cmd);
|
||||
|
||||
/// Undo the top command (no-op if stack is empty).
|
||||
void Undo();
|
||||
|
||||
/// Redo the top undone command (no-op if stack is empty).
|
||||
void Redo();
|
||||
|
||||
/// Clear both stacks (must be called on stage close/open).
|
||||
void Clear();
|
||||
|
||||
bool CanUndo() const { return !m_undoStack.empty(); }
|
||||
bool CanRedo() const { return !m_redoStack.empty(); }
|
||||
|
||||
/// Description of the command that Undo() would reverse, or empty string.
|
||||
std::string GetUndoDescription() const;
|
||||
|
||||
/// Description of the command that Redo() would replay, or empty string.
|
||||
std::string GetRedoDescription() const;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<ICommand>> m_undoStack;
|
||||
std::vector<std::unique_ptr<ICommand>> m_redoStack;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
#include "LayerManager.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <pxr/usd/sdf/layerUtils.h>
|
||||
#include <filesystem>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
LayerManager::LayerManager()
|
||||
: m_stage(nullptr) {
|
||||
}
|
||||
|
||||
LayerManager::~LayerManager() {
|
||||
}
|
||||
|
||||
void LayerManager::SetStage(UsdStageRefPtr stage) {
|
||||
m_stage = stage;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void LayerManager::Refresh() {
|
||||
BuildLayerList();
|
||||
}
|
||||
|
||||
std::vector<LayerInfo> LayerManager::GetLayerStack() const {
|
||||
return m_layers;
|
||||
}
|
||||
|
||||
std::vector<LayerInfo> LayerManager::GetSublayers() const {
|
||||
std::vector<LayerInfo> sublayers;
|
||||
if (!m_stage) return sublayers;
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return sublayers;
|
||||
|
||||
for (const auto& info : m_layers) {
|
||||
if (!info.isRootLayer && !info.isSessionLayer) {
|
||||
sublayers.push_back(info);
|
||||
}
|
||||
}
|
||||
|
||||
return sublayers;
|
||||
}
|
||||
|
||||
bool LayerManager::CreateSublayer(const std::string& identifier, int index) {
|
||||
if (!m_stage) {
|
||||
LOG_ERROR("No stage to create sublayer on");
|
||||
return false;
|
||||
}
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) {
|
||||
LOG_ERROR("No root layer to add sublayer to");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LOG_INFO("Creating sublayer: " + identifier);
|
||||
rootLayer->InsertSubLayerPath(identifier, index);
|
||||
Refresh();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to create sublayer: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerManager::InsertSublayerPath(const std::string& path, int index) {
|
||||
return CreateSublayer(path, index);
|
||||
}
|
||||
|
||||
bool LayerManager::RemoveSublayer(int index) {
|
||||
if (!m_stage) return false;
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
|
||||
try {
|
||||
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
|
||||
if (index < 0 || static_cast<size_t>(index) >= sublayers.size()) {
|
||||
LOG_ERROR("Sublayer index out of range");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Removing sublayer at index: " + std::to_string(index));
|
||||
sublayers.erase(sublayers.begin() + index);
|
||||
Refresh();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to remove sublayer: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerManager::MoveSublayerUp(int index) {
|
||||
if (index <= 0) return false;
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
|
||||
try {
|
||||
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
|
||||
if (static_cast<size_t>(index) >= sublayers.size()) return false;
|
||||
|
||||
std::string temp = sublayers[index];
|
||||
sublayers[index] = sublayers[index - 1];
|
||||
sublayers[index - 1] = temp;
|
||||
Refresh();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to move sublayer: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerManager::MoveSublayerDown(int index) {
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
|
||||
try {
|
||||
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
|
||||
if (index < 0 || static_cast<size_t>(index + 1) >= sublayers.size()) return false;
|
||||
|
||||
std::string temp = sublayers[index];
|
||||
sublayers[index] = sublayers[index + 1];
|
||||
sublayers[index + 1] = temp;
|
||||
Refresh();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to move sublayer: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void LayerManager::MuteLayer(const std::string& layerIdentifier) {
|
||||
if (m_stage) {
|
||||
m_stage->MuteLayer(layerIdentifier);
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void LayerManager::UnmuteLayer(const std::string& layerIdentifier) {
|
||||
if (m_stage) {
|
||||
m_stage->UnmuteLayer(layerIdentifier);
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerManager::IsLayerMuted(const std::string& layerIdentifier) const {
|
||||
if (m_stage) {
|
||||
return m_stage->IsLayerMuted(layerIdentifier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string LayerManager::ExtractDisplayName(const std::string& identifier) {
|
||||
// Try to extract a user-friendly name from the identifier
|
||||
|
||||
// For file paths, use the filename
|
||||
std::filesystem::path path(identifier);
|
||||
if (path.has_filename()) {
|
||||
std::string filename = path.filename().string();
|
||||
if (!filename.empty()) {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
// For anonymous layers or other identifiers, return a short representation
|
||||
if (identifier.find("anon:") == 0) {
|
||||
return "Anonymous Layer";
|
||||
}
|
||||
|
||||
if (identifier.find("session:") == 0) {
|
||||
return "Session Layer";
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
void LayerManager::BuildLayerList() {
|
||||
m_layers.clear();
|
||||
|
||||
if (!m_stage) return;
|
||||
|
||||
try {
|
||||
// Get the full layer stack (root + all sublayers)
|
||||
SdfLayerHandleVector layerStack = m_stage->GetLayerStack();
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
SdfLayerHandle sessionLayer = m_stage->GetSessionLayer();
|
||||
|
||||
for (const auto& layer : layerStack) {
|
||||
LayerInfo info;
|
||||
info.layer = layer;
|
||||
info.identifier = layer->GetIdentifier();
|
||||
info.displayName = ExtractDisplayName(info.identifier);
|
||||
info.realPath = layer->GetRealPath();
|
||||
info.isMuted = layer->IsMuted();
|
||||
info.isAnonymous = layer->IsAnonymous();
|
||||
info.isRootLayer = (layer == rootLayer);
|
||||
info.isSessionLayer = (layer == sessionLayer);
|
||||
|
||||
m_layers.push_back(info);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Built layer list with " + std::to_string(m_layers.size()) + " layers");
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to build layer list: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
struct LayerInfo {
|
||||
pxr::SdfLayerHandle layer;
|
||||
std::string identifier;
|
||||
std::string displayName;
|
||||
std::string realPath;
|
||||
bool isMuted;
|
||||
bool isAnonymous;
|
||||
bool isRootLayer;
|
||||
bool isSessionLayer;
|
||||
};
|
||||
|
||||
class LayerManager {
|
||||
public:
|
||||
LayerManager();
|
||||
~LayerManager();
|
||||
|
||||
void SetStage(pxr::UsdStageRefPtr stage);
|
||||
void Refresh();
|
||||
|
||||
// Layer information
|
||||
std::vector<LayerInfo> GetLayerStack() const;
|
||||
std::vector<LayerInfo> GetSublayers() const;
|
||||
int GetLayerCount() const { return static_cast<int>(m_layers.size()); }
|
||||
|
||||
// Layer operations
|
||||
bool CreateSublayer(const std::string& identifier, int index = -1);
|
||||
bool InsertSublayerPath(const std::string& path, int index = -1);
|
||||
bool RemoveSublayer(int index);
|
||||
bool MoveSublayerUp(int index);
|
||||
bool MoveSublayerDown(int index);
|
||||
|
||||
// Muting
|
||||
void MuteLayer(const std::string& layerIdentifier);
|
||||
void UnmuteLayer(const std::string& layerIdentifier);
|
||||
bool IsLayerMuted(const std::string& layerIdentifier) const;
|
||||
|
||||
// Utility
|
||||
static std::string ExtractDisplayName(const std::string& identifier);
|
||||
|
||||
private:
|
||||
void BuildLayerList();
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
std::vector<LayerInfo> m_layers;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||