Compare commits
83 Commits
ae4a5ab96d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 05201465be | |||
| a907ec0182 | |||
| b5c1e50438 | |||
| e2a4961ebe | |||
| 9d61819636 | |||
| 27c619c518 | |||
| 196a2bb0dd | |||
| 6cb1b45496 | |||
| a877e8973c | |||
| a7314ff108 | |||
| bff42082a5 | |||
| c7edd145df | |||
| 22406fbe63 | |||
| d9826b7bbb | |||
| 7d214821f5 | |||
| 2ddeeed061 | |||
| 038450a1ca | |||
| 0f30e080e3 | |||
| 6b8cb2406e | |||
| c5ca6a7501 | |||
| de94decf64 | |||
| 42fd355c53 | |||
| b95b567b75 | |||
| bb9a5d9428 | |||
| b08bb6b233 | |||
| 09819091c4 | |||
| 338970b243 | |||
| 0640201d5b | |||
| f7a2262e80 | |||
| 781d464f47 | |||
| 7586d380e9 | |||
| 1c55907573 | |||
| 205bf65ab3 | |||
| 862f3c9745 | |||
| 6adf022c13 | |||
| a59b3a5405 | |||
| 45d1cc4391 | |||
| cb0d72d980 | |||
| b02a736da0 | |||
| b31ca69d4f | |||
| 88ae8ccefa | |||
| 46d83d300d | |||
| fb1ae9d3a6 | |||
| 8316e419ba | |||
| 9d8840ced6 | |||
| 4b7323c24c | |||
| e85944ccb6 | |||
| ab934d0818 | |||
| 08f4bd351b | |||
| ddffa5764c | |||
| e8a73674ad | |||
| e0597ae31d | |||
| 425d21db45 | |||
| 0538dd3243 | |||
| c796c76561 | |||
| 2649fe814c | |||
| a24eff04bd | |||
| 4d3cc85ece | |||
| 119a55b1aa | |||
| 3210c16c7f | |||
| 7b1fe91cac | |||
| 81f9e9b4e4 | |||
| 364973fb65 | |||
| 11ce024d6c | |||
| 91fff304b5 | |||
| d280136ca7 | |||
| 81d4372bf6 | |||
| 3ba00761c1 | |||
| f56f4674cc | |||
| 251174ddc3 | |||
| dc0c15de70 | |||
| 7d53eb18d4 | |||
| ee1b750f8c | |||
| 8f309e5440 | |||
| a62764397c | |||
| e5d7967455 | |||
| 7f0fa6a944 | |||
| e0b0fd8204 | |||
| 9816fb8759 | |||
| 9ec9761dd7 | |||
| c4bf9673a8 | |||
| 43c70be574 | |||
| 2a8534eda7 |
@@ -1,5 +1,6 @@
|
||||
# Build directories
|
||||
build/
|
||||
build_*/
|
||||
install/
|
||||
out/
|
||||
.kilocode/
|
||||
@@ -72,3 +73,11 @@ desktop.ini
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# ImGui layout (saved to %APPDATA%\UsdLayerManager at runtime)
|
||||
imgui.ini
|
||||
|
||||
# OCIO config LUT files — downloaded automatically at CMake configure time
|
||||
resources/OpenColorIO-Configs/
|
||||
resources/hdri/
|
||||
resources/vdb/
|
||||
|
||||
@@ -1,4 +1,76 @@
|
||||
# CLAUDE.md — 12-rule template
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
|
||||
OpenUSD 25.11 is expected at `third_party/OpenUSD-v25.11` (set via `CMAKE_PREFIX_PATH` in the preset).
|
||||
|
||||
```powershell
|
||||
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
|
||||
cmake --preset default
|
||||
|
||||
# Build Release (copies all USD/DLL dependencies to build/Release/)
|
||||
cmake --build build --config Release
|
||||
|
||||
# Install to install/bin/
|
||||
cmake --build build --config Release --target install
|
||||
```
|
||||
|
||||
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON` and `HDARNOLD_ROOT` for the maintainer's machine. hdEmbree is bundled directly in the `third_party/OpenUSD-v25.11` build (built with `--embree`), so no separate `HDEMBREE_USD_ROOT` is needed. For a minimal build without optional render delegates, configure manually:
|
||||
|
||||
```powershell
|
||||
cmake -B build -G "Visual Studio 17 2022" `
|
||||
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.11" `
|
||||
-DIMGUI_DIR="third_party/imgui-1.92.7" `
|
||||
-DWITH_CYCLES=OFF
|
||||
```
|
||||
|
||||
**No test runner** — `BUILD_TESTS=OFF` by default. Tests (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths; do not enable without fixing them first.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
**Entry point:** `src/main.cpp` — sets `PXR_PLUGINPATH_NAME`, pre-flight loads each plugin DLL via `LoadLibraryA` (skips missing deps gracefully), then hands off to `Application`.
|
||||
|
||||
**Application** (`src/ui/Application.h/.cpp`) — owns all top-level managers and ImGui panels. Drives the main loop: `Update()` → `RenderUI()`. Persists viewport settings to `%APPDATA%\UsdLayerManager\viewport_settings.ini`.
|
||||
|
||||
**Core managers** (all in `src/core/`):
|
||||
- `UsdStageManager` — opens/closes/saves USD stages; single source of truth for the active `UsdStageRefPtr`.
|
||||
- `LayerManager` — enumerates and manipulates SdfLayer stack (mute, set edit target, add/remove sublayers).
|
||||
- `PropertyManager` — reads/writes prim attributes; drives `PropertyPanel`.
|
||||
- `CommandHistory` — undo/redo stack; all mutating operations go through commands in `src/core/commands/`.
|
||||
|
||||
**Viewport rendering pipeline** (the most complex subsystem):
|
||||
- `ViewportPanel` (`src/ui/`) — container for 1–4 `ViewportTile` instances, handles split layout and shared selection.
|
||||
- `ViewportTile` — owns one `ViewportCamera` + one `UsdSceneRenderer`. Renders into an ImGui child window by passing the scene texture as an ImGui image.
|
||||
- `UsdSceneRenderer` (`src/core/`) — wraps `UsdImagingGLEngine` (Hydra/Storm). Renders into a `GlfDrawTarget` (offscreen RGBA16F FBO). All GL overlay drawing (grid, axis, bbox, camera/light wireframes) happens here while the draw-target FBO is bound.
|
||||
- `ViewportColorCorrector` — **file-local class inside `UsdSceneRenderer.cpp`** (pimpl). Applies sRGB or OCIO color correction as a fullscreen GL pass *after* Hydra renders linear. Hydra's own `HdxColorCorrectionTask` is bypassed (`colorCorrectionMode = "disabled"` always passed to Hydra). See `docs/adr/0001-viewport-color-correction.md`.
|
||||
|
||||
**OCIO config:** `$OCIO` is set at startup (in `Application.cpp`) to `resources/OpenColorIO-Configs/aces_1.2/config.ocio` (downloaded at CMake configure time). The `OcioConfigParser` utility (`src/utils/`) wraps the OCIO C++ API to enumerate displays/views/colorspaces for the UI.
|
||||
|
||||
**UI panels** (all `src/ui/`):
|
||||
- `StageEditorPanel` — edit target, dirty state, drag-drop sublayer ordering.
|
||||
- `SceneHierarchyPanel` — prim tree with type icons.
|
||||
- `PropertyPanel` — attribute inspector/editor.
|
||||
- `TimelinePanel` — transport controls, loop/bounce, frame scrub.
|
||||
- `CurveEditorPanel` — Maya-style F-curve editor with Bezier round-trip to USD time samples.
|
||||
|
||||
**GL loader:** `glad` (generated loader in `src/utils/GLExt.h`). Do not call GL functions before `gladLoadGL`.
|
||||
|
||||
**Logging:** `LOG_INFO / LOG_WARNING / LOG_ERROR` macros (`src/utils/Logger.h`). USD diagnostics (`TF_WARN`, `TF_ERROR`) are routed to the same logger via a `TfDiagnosticMgr::Delegate` installed in `UsdSceneRenderer::InitRenderer()`.
|
||||
|
||||
**DLL layout (Windows):**
|
||||
- Exe + all USD/OCIO/FFmpeg DLLs live in `build/Release/`.
|
||||
- Hydra plugin DLLs live in `build/Release/usd/` (so `plugInfo.json`'s `LibraryPath "../<name>.dll"` resolves correctly).
|
||||
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -13,11 +13,181 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(OpenUSD REQUIRED)
|
||||
find_package(Imgui REQUIRED)
|
||||
find_package(ImguiNodeEditor REQUIRED)
|
||||
find_package(Glad REQUIRED)
|
||||
find_package(FFmpeg REQUIRED)
|
||||
|
||||
# OpenColorIO — provided by the OpenUSD distribution (USD built with OCIO support)
|
||||
set(OpenColorIO_DIR
|
||||
"${OpenUSD_ROOT_DIR}/lib/cmake/OpenColorIO"
|
||||
CACHE PATH "OpenColorIO CMake config dir" FORCE)
|
||||
find_package(OpenColorIO CONFIG REQUIRED)
|
||||
|
||||
# USD build that includes hdEmbree (built with PXR_ENABLE_EMBREE_PLUGIN=ON).
|
||||
# Can be a separate install from OpenUSD_ROOT_DIR; leave empty to search there instead.
|
||||
set(HDEMBREE_USD_ROOT "" CACHE PATH "USD build containing hdEmbree plugin (separate from OpenUSD_ROOT_DIR)")
|
||||
# Only needed if embree*.dll are not inside HDEMBREE_USD_ROOT/bin.
|
||||
set(EMBREE_LOCATION "" CACHE PATH "Embree SDK bin/ directory (leave empty if bundled in HDEMBREE_USD_ROOT/bin)")
|
||||
|
||||
# ── hdEmbree detection ───────────────────────────────────────────────────────
|
||||
if(HDEMBREE_USD_ROOT AND EXISTS "${HDEMBREE_USD_ROOT}")
|
||||
set(_hdembree_root "${HDEMBREE_USD_ROOT}")
|
||||
else()
|
||||
set(_hdembree_root "${OpenUSD_ROOT_DIR}")
|
||||
endif()
|
||||
|
||||
# hdEmbree.dll lives at plugin/usd/hdEmbree.dll; the subdir holds only plugin metadata.
|
||||
set(HDEMBREE_PLUGIN_DIR "${_hdembree_root}/plugin/usd/hdEmbree")
|
||||
set(HDEMBREE_DLL "${_hdembree_root}/plugin/usd/hdEmbree.dll")
|
||||
|
||||
if(EXISTS "${HDEMBREE_PLUGIN_DIR}")
|
||||
set(OPENUSD_HAS_EMBREE TRUE)
|
||||
if(EMBREE_LOCATION AND EXISTS "${EMBREE_LOCATION}")
|
||||
set(_embree_dll_dir "${EMBREE_LOCATION}")
|
||||
else()
|
||||
set(_embree_dll_dir "${_hdembree_root}/bin")
|
||||
endif()
|
||||
# Skip tbb*.dll — already supplied by the main USD install to avoid conflicts.
|
||||
file(GLOB EMBREE_DLLS "${_embree_dll_dir}/embree*.dll")
|
||||
else()
|
||||
set(OPENUSD_HAS_EMBREE FALSE)
|
||||
message(STATUS "hdEmbree: NOT found — set HDEMBREE_USD_ROOT to a USD build with PXR_ENABLE_EMBREE_PLUGIN=ON")
|
||||
endif()
|
||||
|
||||
# ── hdArnold detection ───────────────────────────────────────────────────────
|
||||
# HDARNOLD_ROOT: CMAKE_INSTALL_PREFIX used when building arnold-usd
|
||||
set(HDARNOLD_ROOT "" CACHE PATH "arnold-usd install directory (the one containing plugin/hdArnold.dll)")
|
||||
# ARNOLD_LOCATION: Arnold SDK root (MtoA dir) — needed to deploy ai.dll
|
||||
set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/2026) for ai.dll deployment")
|
||||
|
||||
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
||||
set(OPENUSD_HAS_ARNOLD TRUE)
|
||||
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
||||
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
|
||||
# arnold-usd renamed the ndr plugin to node_registry (USD merged ndr into
|
||||
# sdr as of PXR_VERSION 2505) and added a usdImaging adapter plugin.
|
||||
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold.dll")
|
||||
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold")
|
||||
set(USDIMAGINGARNOLD_DLL "${HDARNOLD_ROOT}/plugin/usdImagingArnold.dll")
|
||||
set(USDIMAGINGARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/usdImagingArnold")
|
||||
if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
|
||||
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
|
||||
# ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
|
||||
file(GLOB ARNOLD_RUNTIME_DLLS
|
||||
"${ARNOLD_LOCATION}/bin/ai*.dll"
|
||||
"${ARNOLD_LOCATION}/bin/AdClm*.dll"
|
||||
"${ARNOLD_LOCATION}/bin/AdskLicensing*.dll"
|
||||
"${ARNOLD_LOCATION}/bin/cer.dll"
|
||||
)
|
||||
else()
|
||||
set(ARNOLD_AI_DLL "")
|
||||
set(ARNOLD_RUNTIME_DLLS "")
|
||||
endif()
|
||||
else()
|
||||
set(OPENUSD_HAS_ARNOLD FALSE)
|
||||
message(STATUS "hdArnold: NOT found — set HDARNOLD_ROOT to the arnold-usd install dir and ARNOLD_LOCATION to the MtoA root")
|
||||
endif()
|
||||
|
||||
# ── Cycles hdCycles Hydra render delegate ────────────────────────────────────
|
||||
# Build Blender Cycles from third_party/cycles using ExternalProject.
|
||||
# Set WITH_CYCLES=ON to enable; requires MSVC, and uses the precompiled libs
|
||||
# bundled in third_party/cycles/lib/windows_x64/.
|
||||
# hdCycles.dll is deployed to usd/ alongside hdEmbree and hdArnold.
|
||||
option(WITH_CYCLES "Build Cycles Hydra render delegate from third_party/cycles" OFF)
|
||||
|
||||
set(CYCLES_BINARY_DIR "${CMAKE_BINARY_DIR}/cycles_build")
|
||||
set(CYCLES_INSTALL_DIR "${CMAKE_BINARY_DIR}/cycles_install")
|
||||
set(HDCYCLES_DLL "${CYCLES_INSTALL_DIR}/hydra/hdCycles.dll")
|
||||
set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
|
||||
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
|
||||
# not in the install output — deploy it explicitly.
|
||||
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")
|
||||
# OpenColorIO_2_5.dll: Cycles' bundled prebuilt OpenImageIO.dll was compiled
|
||||
# against Cycles' own OCIO 2.5 (forced via -DOpenColorIO_DIR below, to avoid
|
||||
# USD's OCIO headers shadowing Cycles' during the ExternalProject build) —
|
||||
# distinct from and required alongside USD's own OpenColorIO_2_2.dll. It also
|
||||
# lives only in the precompiled lib tree, not in cycles_install's output.
|
||||
set(CYCLES_OCIO25_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/bin/OpenColorIO_2_5.dll")
|
||||
|
||||
if(WITH_CYCLES)
|
||||
include(ExternalProject)
|
||||
|
||||
# Resolve absolute USD root so we can forward it to the sub-build.
|
||||
# FindOpenUSD.cmake stores the path in OpenUSD_ROOT_DIR.
|
||||
if(NOT OpenUSD_ROOT_DIR)
|
||||
message(FATAL_ERROR "WITH_CYCLES requires OpenUSD_ROOT_DIR to be set (run find_package(OpenUSD) first).")
|
||||
endif()
|
||||
|
||||
ExternalProject_Add(CyclesBuild
|
||||
SOURCE_DIR "${CMAKE_SOURCE_DIR}/third_party/cycles"
|
||||
BINARY_DIR "${CYCLES_BINARY_DIR}"
|
||||
INSTALL_DIR "${CYCLES_INSTALL_DIR}"
|
||||
CMAKE_ARGS
|
||||
# Always Release — Cycles' Debug build is slow and rarely needed here.
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
|
||||
# Point Cycles at our app's USD so hdCycles is ABI-compatible.
|
||||
-DPXR_ROOT=${OpenUSD_ROOT_DIR}
|
||||
# Hydra delegate is the only deliverable we need.
|
||||
-DWITH_CYCLES_HYDRA_RENDER_DELEGATE=ON
|
||||
-DWITH_CYCLES_USD=ON
|
||||
# Use bundled precompiled libs from lib/windows_x64/.
|
||||
-DWITH_LIBS_PRECOMPILED=ON
|
||||
# Turn off heavy optional features that aren't needed for the delegate.
|
||||
-DWITH_CYCLES_STANDALONE_GUI=OFF
|
||||
-DWITH_CYCLES_DEVICE_CUDA=OFF
|
||||
-DWITH_CYCLES_DEVICE_OPTIX=OFF
|
||||
-DWITH_CYCLES_DEVICE_HIP=OFF
|
||||
-DWITH_CYCLES_DEVICE_ONEAPI=OFF
|
||||
-DWITH_CYCLES_LOGGING=OFF
|
||||
# USD ships OpenVDB v9; Cycles prebuilt is v13 — ABI mismatch, both named
|
||||
# openvdb.dll. Disable openvdb in Cycles to avoid the version conflict.
|
||||
-DWITH_CYCLES_OPENVDB=OFF
|
||||
# Embree4 links tbb12.dll; USD already loads tbb.dll at process startup.
|
||||
# oneTBB detects the older tbb.dll and aborts initialization (DLL_INIT_FAILED).
|
||||
# Disable Embree so Cycles uses its own BVH traversal instead.
|
||||
-DWITH_CYCLES_EMBREE=OFF
|
||||
# ---- pxrConfig.cmake dependency overrides ----
|
||||
# pxrConfig.cmake runs find_dependency() for these before Cycles'
|
||||
# precompiled lib paths are on CMAKE_PREFIX_PATH, so we must supply
|
||||
# them explicitly. The Python3 vars also override the hardcoded
|
||||
# CI build-machine path embedded in pxrConfig.cmake.
|
||||
-DPython3_EXECUTABLE=$ENV{LOCALAPPDATA}/Programs/Python/Python312/python.exe
|
||||
-DPython3_LIBRARY=$ENV{LOCALAPPDATA}/Programs/Python/Python312/libs/python312.lib
|
||||
-DPython3_INCLUDE_DIR=$ENV{LOCALAPPDATA}/Programs/Python/Python312/include
|
||||
-DTBB_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/lib/cmake/TBB
|
||||
-DMaterialX_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/MaterialX
|
||||
# OpenSubdiv and Imath cmake configs are bundled in the USD install.
|
||||
# We must supply their dirs so pxrTargets.cmake can resolve the imported targets.
|
||||
-DOpenSubdiv_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/OpenSubdiv
|
||||
-DImath_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/Imath
|
||||
# Force Cycles to use its own prebuilt OCIO 2.5, not the OCIO 2.1
|
||||
# bundled in the new USD include dir. Without this, the new USD's
|
||||
# include/OpenColorIO/ (OCIO 2.1) shadows Cycles' prebuilt OCIO 2.5
|
||||
# headers and the colorspace.cpp API calls fail to compile.
|
||||
-DOpenColorIO_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/lib/cmake/OpenColorIO
|
||||
BUILD_COMMAND
|
||||
${CMAKE_COMMAND} --build <BINARY_DIR> --config Release
|
||||
INSTALL_COMMAND
|
||||
${CMAKE_COMMAND} --install <BINARY_DIR> --config Release
|
||||
BUILD_BYPRODUCTS
|
||||
"${HDCYCLES_DLL}"
|
||||
)
|
||||
|
||||
set(OPENUSD_HAS_CYCLES TRUE)
|
||||
else()
|
||||
set(OPENUSD_HAS_CYCLES FALSE)
|
||||
message(STATUS "hdCycles: disabled — set WITH_CYCLES=ON to build from third_party/cycles")
|
||||
endif()
|
||||
|
||||
# Collect source files
|
||||
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
|
||||
file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp")
|
||||
# src/ui/NodeEditor is the in-tree vendored imgui-node-editor subset — already
|
||||
# compiled into the imgui_node_editor_impl target by FindImguiNodeEditor.cmake
|
||||
# (with its own required include dirs/defines); excluded here so it isn't
|
||||
# compiled a second time, incorrectly, as part of this target's own sources.
|
||||
list(FILTER UI_SOURCES EXCLUDE REGEX "/src/ui/NodeEditor/")
|
||||
file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp")
|
||||
set(MAIN_SOURCE "src/main.cpp")
|
||||
|
||||
@@ -41,9 +211,24 @@ target_include_directories(UsdLayerManager PRIVATE
|
||||
target_link_libraries(UsdLayerManager PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Imgui::Imgui
|
||||
ImguiNodeEditor::ImguiNodeEditor
|
||||
Glad::Glad
|
||||
FFmpeg::FFmpeg
|
||||
OpenColorIO::OpenColorIO
|
||||
ole32
|
||||
shell32
|
||||
)
|
||||
|
||||
target_include_directories(UsdLayerManager PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
|
||||
)
|
||||
|
||||
# Cycles is an ExternalProject; UsdLayerManager must wait for it to finish
|
||||
# before POST_BUILD copies hdCycles.dll to the output directory.
|
||||
if(WITH_CYCLES)
|
||||
add_dependencies(UsdLayerManager CyclesBuild)
|
||||
endif()
|
||||
|
||||
# Platform-specific settings
|
||||
if(WIN32)
|
||||
# Windows-specific settings
|
||||
@@ -53,6 +238,52 @@ if(WIN32)
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
)
|
||||
|
||||
# OpenColorIO DLL — copy from USD bin/. Filename is version-suffixed
|
||||
# (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
|
||||
# OCIO version changes, so glob for it instead of hardcoding the version.
|
||||
file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
|
||||
if(OPENCOLORIO_RUNTIME_DLL)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${OPENCOLORIO_RUNTIME_DLL}
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying OpenColorIO runtime DLL..."
|
||||
)
|
||||
else()
|
||||
message(WARNING "OpenColorIO runtime DLL not found in ${OpenUSD_ROOT_DIR}/bin")
|
||||
endif()
|
||||
|
||||
# FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
|
||||
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
|
||||
if(FFMPEG_RUNTIME_DLLS)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FFMPEG_RUNTIME_DLLS}
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying FFmpeg runtime DLLs..."
|
||||
)
|
||||
endif()
|
||||
|
||||
# cmake -P script to copy USD bin/ DLLs excluding CRT DLLs (msvcp, vcruntime, etc.).
|
||||
# The USD distribution bundles older CRT DLLs (14.34) that conflict with Cycles
|
||||
# binaries built against the newer system CRT (14.42+). The system CRT is
|
||||
# always present on Windows 10+ — no need to bundle our own copy.
|
||||
file(WRITE "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
|
||||
"file(GLOB _all_dlls \"${OpenUSD_ROOT_DIR}/bin/*.dll\")\n"
|
||||
"set(_dlls)\n"
|
||||
"foreach(_dll \${_all_dlls})\n"
|
||||
" get_filename_component(_name \"\${_dll}\" NAME)\n"
|
||||
" string(TOLOWER \"\${_name}\" _lname)\n"
|
||||
" if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
|
||||
" continue()\n"
|
||||
" endif()\n"
|
||||
" list(APPEND _dlls \"\${_dll}\")\n"
|
||||
"endforeach()\n"
|
||||
"if(_dlls)\n"
|
||||
" file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
|
||||
"endif()\n"
|
||||
)
|
||||
|
||||
# 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>"
|
||||
@@ -60,10 +291,10 @@ if(WIN32)
|
||||
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>"
|
||||
# OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. (CRT DLLs excluded)
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
-P "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/lib/usd"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd"
|
||||
@@ -78,6 +309,120 @@ if(WIN32)
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying runtime DLLs and USD plugins..."
|
||||
)
|
||||
|
||||
if(OPENUSD_HAS_EMBREE)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
# plugInfo.json has LibraryPath "../hdEmbree.dll" relative to usd/hdEmbree/,
|
||||
# which resolves to usd/hdEmbree.dll — so the DLL must live in usd/, not exe root.
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${HDEMBREE_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${HDEMBREE_PLUGIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree"
|
||||
COMMENT "Copying hdEmbree plugin..."
|
||||
)
|
||||
if(EMBREE_DLLS)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${EMBREE_DLLS}
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying Embree runtime DLLs..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(OPENUSD_HAS_ARNOLD)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
# DLLs go in usd/ alongside their metadata dirs (LibraryPath "../hdArnold.dll")
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${HDARNOLD_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${NDRARNOLD_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${HDARNOLD_PLUGIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${NDRARNOLD_PLUGIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${USDIMAGINGARNOLD_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${USDIMAGINGARNOLD_PLUGIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold"
|
||||
COMMENT "Copying hdArnold plugin..."
|
||||
)
|
||||
if(ARNOLD_RUNTIME_DLLS)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${ARNOLD_RUNTIME_DLLS}
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
COMMENT "Copying Arnold runtime DLLs (ai, OIDN, licensing)..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(OPENUSD_HAS_CYCLES)
|
||||
# hdCycles uses the same usd/ layout as hdEmbree:
|
||||
# usd/hdCycles.dll + usd/hdCycles/resources/plugInfo.json
|
||||
# The bundled plugInfo.json has Root=".." and LibraryPath="../hdCycles.dll"
|
||||
# which resolves correctly once the plugin dir lives in usd/.
|
||||
#
|
||||
# Runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, vulkan, etc.)
|
||||
# live in CYCLES_INSTALL_DIR root and must be next to the exe. We can't
|
||||
# file(GLOB) them at configure time (ExternalProject hasn't run yet), so we
|
||||
# write a small cmake -P script that globs them at build time instead.
|
||||
# The script runs at build time (after ExternalProject finishes) so it can glob
|
||||
# the actual install output. CRT/Windows DLLs are excluded — they must come
|
||||
# from the system or the main exe's toolchain, not from Cycles' bundled copy.
|
||||
file(WRITE "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
|
||||
"file(GLOB _all_dlls \"${CYCLES_INSTALL_DIR}/*.dll\")\n"
|
||||
"set(_dlls)\n"
|
||||
"foreach(_dll \${_all_dlls})\n"
|
||||
" get_filename_component(_name \"\${_dll}\" NAME)\n"
|
||||
" string(TOLOWER \"\${_name}\" _lname)\n"
|
||||
# Exclude CRT DLLs (system must supply these; Cycles bundles an older version).
|
||||
" if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
|
||||
" continue()\n"
|
||||
" endif()\n"
|
||||
# Exclude debug-variant DLLs (not needed in release, just wasted space).
|
||||
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
|
||||
# openjph.0.25d.dll → version digit directly followed by "d.dll"
|
||||
# NOTE: must require a digit before the trailing "d" — a bare "d[.]dll$"
|
||||
# also matches legitimate Release DLLs like IlmThread.dll (ends in "...ead.dll").
|
||||
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"[0-9]d[.]dll$\")\n"
|
||||
" continue()\n"
|
||||
" endif()\n"
|
||||
" list(APPEND _dlls \"\${_dll}\")\n"
|
||||
"endforeach()\n"
|
||||
"if(_dlls)\n"
|
||||
" file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
|
||||
"endif()\n"
|
||||
)
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${HDCYCLES_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${HDCYCLES_PLUGIN_DIR}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||
-P "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
|
||||
# tbb12.dll is required by openvdb/embree4 but is not in the install output
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CYCLES_TBB12_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
|
||||
# OpenColorIO_2_5.dll is required by Cycles' bundled OpenImageIO.dll
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CYCLES_OCIO25_DLL}"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/OpenColorIO_2_5.dll"
|
||||
COMMENT "Copying hdCycles plugin and runtime DLLs..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Compiler warnings
|
||||
@@ -100,33 +445,213 @@ install(DIRECTORY ${OpenUSD_BIN_DIR}/
|
||||
)
|
||||
|
||||
# Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR,
|
||||
# OpenImageIO, zlib, etc. These are separate from lib/ and also required at runtime.
|
||||
# OpenImageIO, zlib, etc. Exclude CRT DLLs: the system VC++ Redistributable supplies
|
||||
# these at runtime; bundling an older copy (14.34) causes crashes on machines with
|
||||
# Cycles or other DLLs compiled against the newer CRT (14.42+).
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/
|
||||
DESTINATION bin
|
||||
FILES_MATCHING PATTERN "*.dll"
|
||||
PATTERN "msvcp*" EXCLUDE
|
||||
PATTERN "vcruntime*" EXCLUDE
|
||||
PATTERN "ucrtbase*" EXCLUDE
|
||||
PATTERN "concrt*" EXCLUDE
|
||||
PATTERN "api-ms-win-*" EXCLUDE
|
||||
)
|
||||
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd
|
||||
DESTINATION bin
|
||||
)
|
||||
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd
|
||||
install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd/
|
||||
DESTINATION bin/usd
|
||||
)
|
||||
|
||||
install(FILES ${OpenUSD_ROOT_DIR}/plugin/usd/hdStorm.dll
|
||||
DESTINATION bin
|
||||
)
|
||||
if(OPENUSD_HAS_EMBREE)
|
||||
# Install to bin/usd/ so plugInfo.json's LibraryPath "../hdEmbree.dll" resolves correctly.
|
||||
install(FILES "${HDEMBREE_DLL}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(DIRECTORY "${HDEMBREE_PLUGIN_DIR}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
if(EMBREE_DLLS)
|
||||
install(FILES ${EMBREE_DLLS}
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Install Python runtime DLLs required by OpenUSD.
|
||||
install(FILES
|
||||
if(OPENUSD_HAS_ARNOLD)
|
||||
install(FILES "${HDARNOLD_DLL}" "${NDRARNOLD_DLL}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(DIRECTORY "${HDARNOLD_PLUGIN_DIR}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(DIRECTORY "${NDRARNOLD_PLUGIN_DIR}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(FILES "${USDIMAGINGARNOLD_DLL}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
if(ARNOLD_RUNTIME_DLLS)
|
||||
install(FILES ${ARNOLD_RUNTIME_DLLS}
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(OPENUSD_HAS_CYCLES)
|
||||
install(FILES "${HDCYCLES_DLL}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
install(DIRECTORY "${HDCYCLES_PLUGIN_DIR}"
|
||||
DESTINATION bin/usd
|
||||
OPTIONAL
|
||||
)
|
||||
# Cycles runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, etc.).
|
||||
# No OPTIONAL: install(DIRECTORY) disallows it with FILES_MATCHING, and the
|
||||
# dir always exists here since it is produced by the CyclesBuild step.
|
||||
#
|
||||
# EXCLUDE the MSVC CRT and api-ms-win DLLs that Cycles bundles: they are
|
||||
# older than what the system ships (v14.34 vs system v14.42+), and placing
|
||||
# an older MSVCP140.dll/VCRUNTIME140.dll ahead of System32 on the DLL
|
||||
# search path causes sycl8.dll (built with newer CRT) to fail DllMain with
|
||||
# ERROR_DLL_INIT_FAILED (1114) when hdCycles.dll is loaded.
|
||||
# The system VC++ Redistributable always satisfies these deps at runtime.
|
||||
install(DIRECTORY "${CYCLES_INSTALL_DIR}/"
|
||||
DESTINATION bin
|
||||
FILES_MATCHING
|
||||
PATTERN "*.dll"
|
||||
PATTERN "MSVCP140*.dll" EXCLUDE
|
||||
PATTERN "VCRUNTIME140*.dll" EXCLUDE
|
||||
PATTERN "ucrtbase*.dll" EXCLUDE
|
||||
PATTERN "api-ms-win-*.dll" EXCLUDE
|
||||
PATTERN "OpenColorIO_d_*.dll" EXCLUDE # debug OCIO build
|
||||
PATTERN "openjph.*d.dll" EXCLUDE # debug OpenJPH
|
||||
)
|
||||
# tbb12.dll (used by openvdb/embree4) lives in the precompiled lib tree, not install output
|
||||
install(FILES "${CYCLES_TBB12_DLL}"
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
|
||||
install(FILES "${CYCLES_OCIO25_DLL}"
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install Python runtime DLLs required by OpenUSD and Cycles.
|
||||
# python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
|
||||
# (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
|
||||
set(_python_runtime_dlls
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||
)
|
||||
|
||||
install(FILES ${_python_runtime_dlls}
|
||||
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.
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OCIO: download ACES 1.2 config at configure time if not present
|
||||
# ---------------------------------------------------------------------------
|
||||
set(_ocio_config_dir "${CMAKE_SOURCE_DIR}/resources/OpenColorIO-Configs/aces_1.2")
|
||||
if(NOT EXISTS "${_ocio_config_dir}/config.ocio")
|
||||
message(STATUS "Downloading ACES 1.2 OCIO config (~124 MB) ...")
|
||||
set(_ocio_zip "${CMAKE_BINARY_DIR}/aces_1.2.zip")
|
||||
file(DOWNLOAD
|
||||
"https://github.com/colour-science/OpenColorIO-Configs/releases/download/v1.2/OpenColorIO-Config-ACES-1.2.zip"
|
||||
"${_ocio_zip}"
|
||||
SHOW_PROGRESS
|
||||
STATUS _ocio_dl_status
|
||||
)
|
||||
list(GET _ocio_dl_status 0 _ocio_dl_code)
|
||||
if(_ocio_dl_code EQUAL 0)
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${_ocio_zip}"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
file(COPY "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2/aces_1.2/"
|
||||
DESTINATION "${_ocio_config_dir}")
|
||||
file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2")
|
||||
file(REMOVE "${_ocio_zip}")
|
||||
message(STATUS "ACES 1.2 OCIO config installed to ${_ocio_config_dir}")
|
||||
else()
|
||||
message(WARNING "ACES 1.2 OCIO download failed (${_ocio_dl_status}) — OCIO will not be bundled.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HDRI: shader-ball lighting presets (Poly Haven, CC0) at configure time
|
||||
# ---------------------------------------------------------------------------
|
||||
set(_hdri_dir "${CMAKE_SOURCE_DIR}/resources/hdri")
|
||||
set(_hdri_files
|
||||
kloofendal_48d_partly_cloudy_puresky_1k.exr
|
||||
lebombo_1k.exr
|
||||
artist_workshop_1k.exr
|
||||
venice_sunset_1k.exr
|
||||
)
|
||||
file(MAKE_DIRECTORY "${_hdri_dir}")
|
||||
foreach(_hdri ${_hdri_files})
|
||||
if(NOT EXISTS "${_hdri_dir}/${_hdri}")
|
||||
message(STATUS "Downloading HDRI ${_hdri} ...")
|
||||
file(DOWNLOAD
|
||||
"https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/${_hdri}"
|
||||
"${_hdri_dir}/${_hdri}"
|
||||
STATUS _hdri_dl_status
|
||||
)
|
||||
list(GET _hdri_dl_status 0 _hdri_dl_code)
|
||||
if(NOT _hdri_dl_code EQUAL 0)
|
||||
file(REMOVE "${_hdri_dir}/${_hdri}")
|
||||
message(WARNING "HDRI download failed for ${_hdri} (${_hdri_dl_status}) — the shader-ball lighting preset will fall back to a distant light.")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VDB: cloud volume for the shader-ball preview (OpenVDB sample model) at
|
||||
# configure time if not present
|
||||
# ---------------------------------------------------------------------------
|
||||
set(_vdb_dir "${CMAKE_SOURCE_DIR}/resources/vdb")
|
||||
file(MAKE_DIRECTORY "${_vdb_dir}")
|
||||
if(NOT EXISTS "${_vdb_dir}/bunny_cloud.vdb")
|
||||
message(STATUS "Downloading OpenVDB sample bunny_cloud.vdb ...")
|
||||
set(_vdb_zip "${CMAKE_BINARY_DIR}/bunny_cloud.zip")
|
||||
file(DOWNLOAD
|
||||
"https://artifacts.aswf.io/io/aswf/openvdb/models/bunny_cloud.vdb/1.0.0/bunny_cloud.vdb-1.0.0.zip"
|
||||
"${_vdb_zip}"
|
||||
STATUS _vdb_dl_status
|
||||
)
|
||||
list(GET _vdb_dl_status 0 _vdb_dl_code)
|
||||
if(_vdb_dl_code EQUAL 0)
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${_vdb_zip}"
|
||||
WORKING_DIRECTORY "${_vdb_dir}"
|
||||
)
|
||||
file(REMOVE "${_vdb_zip}")
|
||||
message(STATUS "bunny_cloud.vdb installed to ${_vdb_dir}")
|
||||
else()
|
||||
message(WARNING "OpenVDB sample download failed (${_vdb_dl_status}) — the Cloud Volume preview shape will be unavailable.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Copy resources to build directory
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/resources
|
||||
DESTINATION ${CMAKE_BINARY_DIR}
|
||||
@@ -142,7 +667,42 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
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"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/hdri"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/preview"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/preview"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/vdb"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
|
||||
# MaterialX standard library (nodedefs + genosl headers such as mx_funcs.h).
|
||||
# hdArnold reads PXR_MTLX_STDLIB_SEARCH_PATHS to locate these; without them
|
||||
# Arnold's OSL compile of MaterialX shaders fails with
|
||||
# "fatal error: 'mx_funcs.h' file not found". Application.cpp points the
|
||||
# env var at this deployed copy.
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${OpenUSD_ROOT_DIR}/libraries"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/libraries"
|
||||
COMMENT "Copying fonts, SVG icons, HDRI presets, preview shapes and MaterialX stdlib to build output"
|
||||
)
|
||||
|
||||
# Copy ACES OCIO config to build output — only on first build (skipped if already present).
|
||||
if(EXISTS "${_ocio_config_dir}/config.ocio")
|
||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DSRC=${_ocio_config_dir}"
|
||||
"-DDST=$<TARGET_FILE_DIR:UsdLayerManager>/resources/OpenColorIO-Configs/aces_1.2"
|
||||
-P "${CMAKE_SOURCE_DIR}/cmake/CopyDirIfMissing.cmake"
|
||||
COMMENT "Copying ACES OCIO config to build output (first time only)"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
|
||||
install(FILES
|
||||
${OPENCOLORIO_RUNTIME_DLL}
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
# Install resources (icons, fonts placeholder) next to the exe so the
|
||||
@@ -151,6 +711,14 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
|
||||
DESTINATION bin
|
||||
)
|
||||
|
||||
# MaterialX standard library — required by hdArnold's OSL codegen (mx_funcs.h).
|
||||
# See the matching POST_BUILD copy above and PXR_MTLX_STDLIB_SEARCH_PATHS in
|
||||
# Application.cpp.
|
||||
install(DIRECTORY "${OpenUSD_ROOT_DIR}/libraries"
|
||||
DESTINATION bin
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -168,6 +736,26 @@ 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 "hdEmbree support: ${OPENUSD_HAS_EMBREE}")
|
||||
if(OPENUSD_HAS_EMBREE AND EMBREE_DLLS)
|
||||
message(STATUS "Embree DLLs: ${EMBREE_DLLS}")
|
||||
elseif(OPENUSD_HAS_EMBREE)
|
||||
message(WARNING "hdEmbree found but no Embree DLLs detected — set EMBREE_LOCATION to the Embree SDK bin/ dir or check HDEMBREE_USD_ROOT/bin")
|
||||
endif()
|
||||
message(STATUS "hdArnold support: ${OPENUSD_HAS_ARNOLD}")
|
||||
if(OPENUSD_HAS_ARNOLD)
|
||||
message(STATUS "hdArnold DLL: ${HDARNOLD_DLL}")
|
||||
message(STATUS "Arnold runtime DLLs: ${ARNOLD_RUNTIME_DLLS}")
|
||||
if(NOT ARNOLD_AI_DLL)
|
||||
message(WARNING "hdArnold found but ai.dll not located — set ARNOLD_LOCATION to the MtoA root (e.g. C:/Autodesk/mtoa/5.5.0/2026)")
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "hdCycles support: ${OPENUSD_HAS_CYCLES}")
|
||||
if(OPENUSD_HAS_CYCLES)
|
||||
message(STATUS "Cycles source: ${CMAKE_SOURCE_DIR}/third_party/cycles")
|
||||
message(STATUS "Cycles build: ${CYCLES_BINARY_DIR}")
|
||||
message(STATUS "Cycles install: ${CYCLES_INSTALL_DIR}")
|
||||
endif()
|
||||
message(STATUS "=======================================")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -186,11 +774,13 @@ add_executable(ViewportDisplayTest
|
||||
target_include_directories(ViewportDisplayTest PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
|
||||
)
|
||||
|
||||
target_link_libraries(ViewportDisplayTest PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Glad::Glad
|
||||
FFmpeg::FFmpeg
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
@@ -232,11 +822,13 @@ add_executable(RendererDiagnosticTest
|
||||
target_include_directories(RendererDiagnosticTest PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
|
||||
)
|
||||
|
||||
target_link_libraries(RendererDiagnosticTest PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Glad::Glad
|
||||
FFmpeg::FFmpeg
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
|
||||
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD_v25.05",
|
||||
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.11",
|
||||
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
|
||||
"CMAKE_CXX_STANDARD": "17",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"BUILD_TESTS": "ON"
|
||||
"BUILD_TESTS": "OFF",
|
||||
"WITH_CYCLES": "ON",
|
||||
"HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
|
||||
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,7 +35,7 @@
|
||||
"description": "Optimized release build",
|
||||
"inherits": "default",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
|
||||
"BUILD_TESTS": "OFF"
|
||||
}
|
||||
},
|
||||
@@ -64,7 +67,8 @@
|
||||
"name": "release",
|
||||
"displayName": "Release Build",
|
||||
"configurePreset": "release",
|
||||
"configuration": "Release"
|
||||
"configuration": "Release",
|
||||
"targets": ["install"]
|
||||
},
|
||||
{
|
||||
"name": "no-tests",
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
# 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.
|
||||
A C++17 / Windows desktop application providing a **Maya-style** USD scene and look-dev editor using **OpenUSD v25.11**, **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, author UsdShade material graphs with a live shader-ball preview, animate with a Bezier curve editor, and interact with 3D scenes through a multi-viewport Hydra renderer with OCIO color management.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
- **Layer Stack Management** — `StageEditorPanel`: create, reorder, mute/unmute sublayers, set the edit target, add existing files as sublayers, with full undo support
|
||||
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename, reparent, and group 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
|
||||
- **Material Editor** — Hypershade-style node-graph editor for UsdShade/MaterialX networks (`MaterialEditorPanel`): material browser, drag-to-connect node canvas, per-node/per-pin display modes, Sdr-driven node creation menu and enum dropdowns, colorSpace authoring on texture inputs, and a live shader-ball preview (`MaterialPreviewRenderer`) with its own renderer/HDRI/preview-shape controls. In-canvas node thumbnails (`NodeThumbnailCache`) show texture previews and per-node shader-ball renders, amortized across frames and re-rendered until progressive delegates (Arnold/Cycles/Embree) fully converge
|
||||
- **Timeline & Curve Editor** — Transport controls with loop/bounce and Auto-Key (`TimelinePanel`); a Maya-style F-curve editor (`CurveEditorPanel`) with Bezier round-trip to USD time samples; Playblast export to MP4 via `MovieEncoder` (libavcodec)
|
||||
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); OCIO-based color management (sRGB or full display/view/look pipeline) applied as a post-Hydra GL pass; grid overlay; selection bounding boxes; camera/light wireframes
|
||||
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera, render delegate, and AOV; Space to maximize/restore
|
||||
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages
|
||||
- **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
|
||||
- **Undo/Redo** — Full command history (`ICommand` subclasses under `src/core/commands/`) covering prim creation/deletion/reparenting/grouping, attribute edits, transform changes, shader-node creation/connection, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration
|
||||
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing
|
||||
|
||||
| Material Editor (Arnold) | Curve Editor | Quad Viewport (4 delegates) | MaterialX import |
|
||||
|---|---|---|---|
|
||||
|  |  |  |  |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Version | Notes |
|
||||
|---|---|---|
|
||||
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.05 | Prebuilt; found via `FindOpenUSD.cmake` |
|
||||
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.11 | 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`) |
|
||||
|
||||
### Optional Render Delegates
|
||||
|
||||
| Delegate | Renderer Version | Variable | Notes |
|
||||
|---|---|---|---|
|
||||
| **hdEmbree** | Embree 4.3.3 | `HDEMBREE_USD_ROOT` (optional) | Bundled directly in `third_party/OpenUSD-v25.11` (built with `--embree`); set `HDEMBREE_USD_ROOT` only if using a separate USD build, and `EMBREE_LOCATION` if Embree DLLs aren't in its `bin/` |
|
||||
| **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) (tag `Arnold-7.4.5.1`, for USD 25.11's `ndr`→`sdr` API change) install dir + MtoA root for `ai.dll` |
|
||||
| **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC |
|
||||
|
||||
All delegate paths are pre-configured in `CMakePresets.json`.
|
||||
|
||||
## Build
|
||||
|
||||
### Prerequisites
|
||||
@@ -40,16 +57,19 @@ cmake --build build --config Release
|
||||
cmake --install build --config Release
|
||||
```
|
||||
|
||||
The installed `App.exe` lives in `install/bin/`.
|
||||
The installed executable lives in `install/bin/`.
|
||||
|
||||
**Presets** — `default` (Release + tests), `debug`, `release`, `no-tests`.
|
||||
**Presets** — `default` (`Release`, `WITH_CYCLES=ON`, all render delegates), `debug` (`Debug`), `release` (`RelWithDebInfo`, installs), `no-tests` (`Release`). All presets currently build with `BUILD_TESTS=OFF`.
|
||||
|
||||
### Building with Cycles
|
||||
|
||||
`WITH_CYCLES=ON` is set in the default preset. The first build compiles Blender Cycles from `third_party/cycles` via ExternalProject — expect a significantly longer initial build. Subsequent builds are incremental.
|
||||
|
||||
Cycles runtime dependencies (`embree4.dll`, `openvdb.dll`, `OpenImageDenoise.dll`, `tbb12.dll`, etc.) are automatically deployed to the output directory by a post-build step. CRT DLLs bundled inside the Cycles install are excluded to avoid version conflicts with the app's own MSVC runtime.
|
||||
|
||||
### Tests
|
||||
|
||||
```powershell
|
||||
cmake --build build --config Release
|
||||
ctest --test-dir build -C Release
|
||||
```
|
||||
There is currently no working test runner: `BUILD_TESTS` is `OFF` in every preset. The two test executables (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths and don't build — fix those before turning `BUILD_TESTS` on.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -59,24 +79,47 @@ src/
|
||||
├── 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
|
||||
│ ├── PropertyManager — Property read/write via edit target with undo
|
||||
│ ├── MaterialManager — UsdShade graph introspection + Sdr node-type/enum discovery
|
||||
│ ├── CommandHistory — Undo/redo stack for ICommand instances
|
||||
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays
|
||||
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays + OCIO color correction
|
||||
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera
|
||||
│ └── commands/ — ICommand subclasses for all mutable operations
|
||||
│ ├── TransformCommand — Undoable translate/rotate/scale
|
||||
│ ├── CreatePrimCommand — Undoable prim creation (with camera orientation)
|
||||
│ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot)
|
||||
│ ├── RenamePrimCommand — Undoable prim rename
|
||||
│ ├── ReparentPrimCommand — Undoable reparent via UsdNamespaceEditor
|
||||
│ ├── GroupPrimsCommand — Group selected prims under a new Xform
|
||||
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes
|
||||
│ ├── AddReferenceCommand — Undoable reference addition
|
||||
│ ├── ReplaceReferenceCommand — Undoable reference replacement
|
||||
│ ├── CreateShaderNodeCommand — Undoable UsdShadeShader creation (Material Editor)
|
||||
│ ├── ConnectShaderAttrsCommand — Undoable shader input↔output connection
|
||||
│ ├── DisconnectShaderAttrCommand — Undoable shader connection removal
|
||||
│ └── LayerCommands — Create, remove, reorder sublayers
|
||||
├── 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
|
||||
│ ├── Application — App shell (owns managers/panels, docking, menus)
|
||||
│ ├── ImGuiContext — Win32+OpenGL+ImGui initialization and loop
|
||||
│ ├── ViewportPanel — Viewport container (layout, divider drag, maximize)
|
||||
│ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking)
|
||||
│ ├── SceneHierarchyPanel — Prim tree browser with context menus
|
||||
│ ├── PropertyPanel — Attribute/transform editor (channel box-style)
|
||||
│ ├── StageEditorPanel — Layer stack editor (edit target, dirty state, drag-drop ordering)
|
||||
│ ├── TimelinePanel — Transport controls, loop/bounce, frame scrub, Playblast
|
||||
│ ├── CurveEditorPanel — Maya-style F-curve editor (Bezier ↔ USD time samples)
|
||||
│ ├── MaterialEditorPanel — Hypershade-style UsdShade node-graph editor
|
||||
│ ├── MaterialPreviewRenderer — Shader-ball preview: independent Hydra instance + scratch stage
|
||||
│ ├── NodeThumbnailCache — Async texture decode + amortized shader-ball thumbnails for the node canvas
|
||||
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
|
||||
│ ├── IconManager — SVG icon rasterization via NanoSVG
|
||||
│ └── NodeEditor/ — Vendored/patched imgui-node-editor
|
||||
└── utils/ — Cross-cutting utilities
|
||||
├── Logger — Thread-safe logging (file + console)
|
||||
├── Logger — Thread-safe logging (file + console); also sinks USD TF_WARN/TF_ERROR
|
||||
├── FileDialog — Win32 file open/save dialogs
|
||||
├── PathUtils — Exe-relative path resolution
|
||||
├── OcioConfigParser — Enumerates displays/views/colorspaces/looks from the active OCIO config
|
||||
├── MovieEncoder — Streams RGBA frames to H.264/MP4 via libavcodec (Playblast)
|
||||
└── GLExt — GLAD extension initialization
|
||||
```
|
||||
|
||||
@@ -86,6 +129,7 @@ src/
|
||||
- **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
|
||||
- **Own Color Correction Pass** — Hydra's `HdxColorCorrectionTask` is bypassed; a file-local `ViewportColorCorrector` (inside `UsdSceneRenderer.cpp`) applies sRGB/OCIO as a fullscreen GL pass after Hydra renders linear (see `docs/adr/0001-viewport-color-correction.md`)
|
||||
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
|
||||
|
||||
## Project Configuration
|
||||
@@ -96,6 +140,7 @@ src/
|
||||
| `cmake/modules/FindImgui.cmake` | ImGui source integration |
|
||||
| `cmake/modules/FindGlad.cmake` | GLAD loader setup |
|
||||
| `CMakePresets.json` | Build presets (VS 17 2022, x64) |
|
||||
| `docs/adr/` | Architecture decision records |
|
||||
| `AGENTS.md` | AI agent build/architecture guide |
|
||||
| `.kilo/` | Kilo AI configuration (commands, agents, skills) |
|
||||
| `openspec/changes/` | Feature proposals and implementation tracking |
|
||||
@@ -114,21 +159,21 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
|
||||
|
||||
- **Python 3.12** — OpenUSD requires `python312.dll` at runtime. CMake copies it to the output directory via `POST_BUILD` commands.
|
||||
- **USD Plugin Path** — The app scans `<exe_dir>/usd/` for `plugInfo.json` and registers plugins via `pxr::PlugRegistry`.
|
||||
- **Render delegate DLLs** — hdEmbree, hdArnold, and hdCycles each bring their own runtime dependencies (Embree, Arnold SDK, Cycles libs). CMake `POST_BUILD` steps deploy all required DLLs next to the executable automatically.
|
||||
|
||||
## Contributing
|
||||
|
||||
Before writing any OpenUSD API call, verify the API exists in the actual SDK:
|
||||
|
||||
```powershell
|
||||
findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\
|
||||
findstr /r /s "FunctionName" third_party\OpenUSD-v25.11\include\
|
||||
```
|
||||
|
||||
Always build and install before running tests:
|
||||
Always build and install before manually verifying a change:
|
||||
|
||||
```powershell
|
||||
cmake --build build --config Release
|
||||
cmake --install build --config Release
|
||||
ctest --test-dir build -C Release
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# FindFFmpeg.cmake
|
||||
# Locates the prebuilt FFmpeg dev package in third_party/ffmpeg.
|
||||
# Sets up IMPORTED targets: FFmpeg::avcodec FFmpeg::avformat
|
||||
# FFmpeg::avutil FFmpeg::swscale
|
||||
# and an umbrella: FFmpeg::FFmpeg
|
||||
|
||||
set(FFMPEG_ROOT "${CMAKE_SOURCE_DIR}/third_party/ffmpeg"
|
||||
CACHE PATH "FFmpeg install root (contains bin/ include/ lib/)")
|
||||
|
||||
set(FFmpeg_FOUND TRUE)
|
||||
|
||||
foreach(_comp avcodec avformat avutil swscale)
|
||||
# Import library (.lib) — required for MSVC linking
|
||||
find_library(FFmpeg_${_comp}_LIB
|
||||
NAMES ${_comp}
|
||||
PATHS "${FFMPEG_ROOT}/lib"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
# Runtime DLL — name includes a version suffix, e.g. avcodec-62.dll
|
||||
file(GLOB _dlls "${FFMPEG_ROOT}/bin/${_comp}-*.dll")
|
||||
if(_dlls)
|
||||
list(GET _dlls 0 FFmpeg_${_comp}_DLL)
|
||||
else()
|
||||
set(FFmpeg_${_comp}_DLL "")
|
||||
endif()
|
||||
|
||||
if(NOT FFmpeg_${_comp}_LIB)
|
||||
set(FFmpeg_FOUND FALSE)
|
||||
message(STATUS "FFmpeg: ${_comp}.lib NOT found in ${FFMPEG_ROOT}/lib")
|
||||
continue()
|
||||
endif()
|
||||
if(NOT FFmpeg_${_comp}_DLL)
|
||||
set(FFmpeg_FOUND FALSE)
|
||||
message(STATUS "FFmpeg: ${_comp}-*.dll NOT found in ${FFMPEG_ROOT}/bin")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
add_library(FFmpeg::${_comp} SHARED IMPORTED)
|
||||
set_target_properties(FFmpeg::${_comp} PROPERTIES
|
||||
IMPORTED_IMPLIB "${FFmpeg_${_comp}_LIB}"
|
||||
IMPORTED_LOCATION "${FFmpeg_${_comp}_DLL}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_ROOT}/include"
|
||||
)
|
||||
message(STATUS "FFmpeg: ${_comp} -> ${FFmpeg_${_comp}_LIB}")
|
||||
endforeach()
|
||||
|
||||
if(FFmpeg_FOUND)
|
||||
# Umbrella target so callers can just link FFmpeg::FFmpeg
|
||||
if(NOT TARGET FFmpeg::FFmpeg)
|
||||
add_library(FFmpeg::FFmpeg INTERFACE IMPORTED)
|
||||
set_target_properties(FFmpeg::FFmpeg PROPERTIES
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"FFmpeg::avcodec;FFmpeg::avformat;FFmpeg::avutil;FFmpeg::swscale"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"${FFMPEG_ROOT}/include"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(FFmpeg
|
||||
REQUIRED_VARS FFmpeg_avcodec_LIB FFmpeg_avformat_LIB
|
||||
FFmpeg_avutil_LIB FFmpeg_swscale_LIB)
|
||||
@@ -0,0 +1,86 @@
|
||||
# FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor
|
||||
#
|
||||
# This module looks for imgui-node-editor at src/ui/NodeEditor (an in-tree,
|
||||
# version-controlled subset of the upstream library — MIT licensed, see
|
||||
# src/ui/NodeEditor/LICENSE — containing only the files this project's build
|
||||
# actually compiles; the full upstream drop, including the standalone example
|
||||
# app used to reproduce library-level bugs against a pure reference build,
|
||||
# stays vendored separately at third_party/imgui-node-editor) and compiles its
|
||||
# sources directly against this project's own Imgui::Imgui target so it shares
|
||||
# the exact same ImGui headers/defines (no second copy of ImGui is pulled in).
|
||||
#
|
||||
# Inputs:
|
||||
# IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root
|
||||
#
|
||||
# Outputs:
|
||||
# ImguiNodeEditor_FOUND
|
||||
# ImguiNodeEditor::ImguiNodeEditor (imported STATIC target)
|
||||
|
||||
if(NOT IMGUI_NODE_EDITOR_DIR)
|
||||
set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor")
|
||||
endif()
|
||||
|
||||
if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR)
|
||||
set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR
|
||||
"${CMAKE_SOURCE_DIR}/src/ui/NodeEditor/BlueprintWidgets")
|
||||
endif()
|
||||
|
||||
find_path(ImguiNodeEditor_INCLUDE_DIR
|
||||
NAMES imgui_node_editor.h
|
||||
PATHS "${IMGUI_NODE_EDITOR_DIR}/Include"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
set(ImguiNodeEditor_SOURCES
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/crude_json.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_canvas.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor_api.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor.cpp"
|
||||
# Blueprint pin-icon widgets (Widgets/Drawing only — Builders.cpp is
|
||||
# intentionally excluded, it needs Spring()/BeginHorizontal()/BeginVertical()
|
||||
# from thedmd's own ImGui fork, which this project's vanilla ImGui lacks).
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Drawing.cpp"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Widgets.cpp"
|
||||
)
|
||||
|
||||
foreach(_src ${ImguiNodeEditor_SOURCES})
|
||||
if(NOT EXISTS "${_src}")
|
||||
set(ImguiNodeEditor_SOURCES_MISSING TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(ImguiNodeEditor
|
||||
REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR
|
||||
FAIL_MESSAGE "imgui-node-editor not found — expected it at src/ui/NodeEditor"
|
||||
)
|
||||
|
||||
if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING)
|
||||
message(FATAL_ERROR "imgui-node-editor sources missing under ${IMGUI_NODE_EDITOR_DIR}/Source")
|
||||
endif()
|
||||
|
||||
if(ImguiNodeEditor_FOUND AND NOT TARGET ImguiNodeEditor::ImguiNodeEditor)
|
||||
add_library(imgui_node_editor_impl STATIC ${ImguiNodeEditor_SOURCES})
|
||||
|
||||
target_include_directories(imgui_node_editor_impl
|
||||
PUBLIC "${ImguiNodeEditor_INCLUDE_DIR}"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Include"
|
||||
PRIVATE "${IMGUI_NODE_EDITOR_DIR}/Source"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source"
|
||||
)
|
||||
|
||||
# Must be defined before the first #include <imgui.h> in every translation
|
||||
# unit (imgui_internal.h hard-errors otherwise) — a command-line define
|
||||
# guarantees that ordering regardless of vendored include order.
|
||||
target_compile_definitions(imgui_node_editor_impl PRIVATE IMGUI_DEFINE_MATH_OPERATORS)
|
||||
|
||||
# Must share this project's own ImGui build (same headers/defines) rather
|
||||
# than pulling in a second ImGui — imgui-node-editor only needs the public
|
||||
# API plus ImRect/ImFloor from imgui_internal.h.
|
||||
target_link_libraries(imgui_node_editor_impl PUBLIC Imgui::Imgui)
|
||||
|
||||
add_library(ImguiNodeEditor::ImguiNodeEditor ALIAS imgui_node_editor_impl)
|
||||
message(STATUS "imgui-node-editor found: ${ImguiNodeEditor_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
mark_as_advanced(ImguiNodeEditor_INCLUDE_DIR)
|
||||
@@ -59,6 +59,7 @@ set(OpenUSD_REQUIRED_LIBS
|
||||
usdGeom
|
||||
usdLux
|
||||
usdShade
|
||||
usdVol
|
||||
usdImaging
|
||||
usdImagingGL
|
||||
hdx
|
||||
@@ -71,7 +72,6 @@ set(OpenUSD_REQUIRED_LIBS
|
||||
hio
|
||||
ar
|
||||
kind
|
||||
ndr
|
||||
sdr
|
||||
python
|
||||
cameraUtil
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# ADR 0001 — Custom Viewport Color Correction (sRGB / OCIO)
|
||||
|
||||
- **Status:** Superseded (2026-06-30) — replaced by the stageView.py approach (HdxColorCorrectionTask via RenderParams)
|
||||
- **Date:** 2026-06-27
|
||||
- **Component:** `src/core/UsdSceneRenderer.cpp` (`ViewportColorCorrector`, `ApplyViewportColorCorrection`)
|
||||
- **Commit:** `fb1ae9d`
|
||||
|
||||
## Context
|
||||
|
||||
The viewport renders a USD stage through `UsdImagingGLEngine` (Hydra / Storm)
|
||||
into an offscreen `GlfDrawTarget`, whose color texture ImGui samples for
|
||||
display. Color management (linear → display) was delegated to Hydra's
|
||||
`HdxColorCorrectionTask` via `UsdImagingGLEngine::SetColorCorrectionSettings()`,
|
||||
configured by the per-viewport `ColorCorrectionMode` (Disabled / sRGB /
|
||||
OpenColorIO) plus OCIO display/view/colorspace/look.
|
||||
|
||||
Two problems made the OCIO path unusable:
|
||||
|
||||
1. **OCIO output was black.** sRGB correction rendered correctly through the
|
||||
same color AOV, proving the correction *input* was fine — the hdx OCIO
|
||||
shader/LUT path itself emitted black. The exact cause stayed opaque because
|
||||
USD reports it via `TF_WARN`, which this GUI app discarded (no console).
|
||||
|
||||
2. **The failure was sticky.** OCIO resources are built lazily *inside*
|
||||
`Render()` (`HdxColorCorrectionTask::Sync`), not in `SetColorCorrectionSettings()`.
|
||||
A throw there escaped `UsdSceneRenderer::Render()` and skipped
|
||||
`m_drawTarget->Unbind()`, permanently unbalancing the `GlfDrawTarget`
|
||||
bind/restore stack — so every later frame, **including after switching back
|
||||
to sRGB**, stayed black until an app restart.
|
||||
|
||||
We do not control the bundled USD build, so fixing `HdxColorCorrectionTask`
|
||||
itself was not an option. App and USD both link OpenColorIO 2.1, and a usable
|
||||
ACES 1.2 config is bundled and pointed to via `$OCIO`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Correct, non-black OCIO display transforms in the viewport (ACES 1.2).
|
||||
- sRGB output visually identical to the previous hdx sRGB path.
|
||||
- A color-management failure can never permanently corrupt the viewport.
|
||||
- Overlays (grid, axis, bbox, camera/light gizmos) remain display-referred,
|
||||
drawn on top of the corrected image — unchanged from the prior behavior.
|
||||
|
||||
**Non-Goals:**
|
||||
- Replacing Hydra's *rendering* — only its color-correction stage is replaced.
|
||||
- Exposure/gamma dynamic-property UI, or interactive grading.
|
||||
- Applying an explicit OCIO look override beyond the view's own looks.
|
||||
- Color-managing the ImGui UI chrome outside the viewport image.
|
||||
|
||||
## Decision
|
||||
|
||||
### D1 — Replace `HdxColorCorrectionTask` with our own GL post-process
|
||||
|
||||
**Decision:** Hydra always renders **linear** (`colorCorrectionMode = "disabled"`).
|
||||
A custom fullscreen pass (`ViewportColorCorrector`) applies the correction
|
||||
afterwards, fully under our control.
|
||||
|
||||
**Rationale:** The hdx OCIO path is a black box we cannot patch in the bundled
|
||||
USD. Owning the post-process gives deterministic, debuggable behavior and
|
||||
removes the lazy-throw-inside-Render hazard entirely. sRGB and OCIO now share
|
||||
one code path with consistent ordering.
|
||||
|
||||
---
|
||||
|
||||
### D2 — Render linear into an `RGBA16F` color attachment
|
||||
|
||||
**Decision:** The draw-target color attachment was changed from `GL_RGBA`
|
||||
(8-bit unorm) to `GL_RGBA16F`.
|
||||
|
||||
**Rationale:** We now store the *linear* scene as the intermediate. Display
|
||||
transforms (especially ACES) need scene values outside `[0,1]`; an 8-bit unorm
|
||||
buffer would clip highlights and band the shadows before correction even runs.
|
||||
16F is ample headroom at negligible cost. ImGui and `CaptureFrame` sample/read
|
||||
it unchanged.
|
||||
|
||||
---
|
||||
|
||||
### D3 — OCIO via the OCIO GPU API (`GpuShaderDesc`), LUTs uploaded by us
|
||||
|
||||
**Decision:** For OCIO mode, build the transform directly with OCIO 2.1:
|
||||
`config->getProcessor(srcColorSpace, display, view, FORWARD)` →
|
||||
`getDefaultGPUProcessor()` → `extractGpuShaderInfo(GpuShaderDesc)` with
|
||||
`GPU_LANGUAGE_GLSL_1_3` and function name `OCIODisplay`. We compile the
|
||||
generated GLSL into our fragment shader and upload/bind every 1D/2D/3D LUT it
|
||||
requests as GL textures. The source colorspace defaults to the `scene_linear`
|
||||
role when unset.
|
||||
|
||||
**Rationale:** This is exactly what `HdxColorCorrectionTask` does internally,
|
||||
but visible and ours to debug. `GLSL_1_3` matches the project's existing
|
||||
`#version 130` shaders and emits modern `texture()` calls. The program and LUTs
|
||||
are cached and rebuilt only when display/view/colorspace/look change.
|
||||
|
||||
---
|
||||
|
||||
### D4 — Graceful degradation to sRGB; never black
|
||||
|
||||
**Decision:** Any OCIO failure — processor build, shader compile/link, or LUT
|
||||
upload — logs the reason and falls back to the sRGB encode shader for that
|
||||
frame. The failing key is remembered so we do not retry (and re-spam logs)
|
||||
until the parameters change.
|
||||
|
||||
**Rationale:** A misconfigured display/view must degrade to a usable image, not
|
||||
a black viewport. sRGB is the correct neutral fallback and matches the default.
|
||||
|
||||
---
|
||||
|
||||
### D5 — Implemented inline in `UsdSceneRenderer.cpp` (pimpl), not new TU/files
|
||||
|
||||
**Decision:** `ViewportColorCorrector` is a class defined in
|
||||
`UsdSceneRenderer.cpp`, held via `std::unique_ptr` behind a forward declaration
|
||||
in the header.
|
||||
|
||||
**Rationale:** `CORE_SOURCES` is a `file(GLOB_RECURSE ...)`; adding a new `.cpp`
|
||||
forces a CMake reconfigure, which risks re-triggering the heavy Cycles
|
||||
`ExternalProject` build. Keeping it in an existing TU avoids that while the
|
||||
pimpl keeps OCIO/GL includes out of the public header. The corrector follows
|
||||
the file's existing `Init*/Destroy*` GL-resource lifecycle conventions.
|
||||
|
||||
---
|
||||
|
||||
### D6 — Route USD diagnostics to the app log
|
||||
|
||||
**Decision:** Install a process-wide `TfDiagnosticMgr::Delegate`
|
||||
(`InstallUsdDiagnosticLogger`) that forwards USD `TF_ERROR` / `TF_WARN` /
|
||||
`TF_STATUS` to the app logger with a `[USD]` prefix.
|
||||
|
||||
**Rationale:** USD's own diagnostics were invisible in this GUI app. This made
|
||||
the original OCIO failure undiagnosable and is generally useful for any Hydra
|
||||
issue.
|
||||
|
||||
## Pipeline
|
||||
|
||||
Per frame, after Hydra renders linear into the draw target:
|
||||
|
||||
1. `m_drawTarget->Resolve()` — resolves MSAA into the color attachment (no-op
|
||||
when MSAA is off), giving a single-sample linear image.
|
||||
2. Blit that attachment into `m_ccLinearTex` — a separate copy is required
|
||||
because sampling and writing the same texture in one pass is illegal.
|
||||
3. Bind the draw-target render FBO (the MSAA FBO when multisampling) and draw
|
||||
the corrected fullscreen triangle sampling `m_ccLinearTex`.
|
||||
4. Overlays draw on top (uncorrected), then the final `Resolve()` in
|
||||
`GetColorTextureID()` produces the texture ImGui samples.
|
||||
|
||||
This keeps overlays display-referred exactly as before, and the correction is
|
||||
MSAA-correct (a flat fullscreen quad resolves to the same value per sample).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- OCIO display transforms work; OCIO failures degrade to sRGB, never black.
|
||||
- A color-correction failure can no longer corrupt the bind stack — the throw
|
||||
hazard is gone because Hydra no longer does OCIO.
|
||||
- The full transform is inspectable/loggable in our own code.
|
||||
- HDR-linear intermediate enables correct tone-mapped output.
|
||||
|
||||
**Negative / trade-offs:**
|
||||
- We reimplement and now maintain OCIO GPU plumbing (LUT upload, sampler
|
||||
binding) that USD would otherwise own; OCIO major-version API changes could
|
||||
require updates.
|
||||
- One extra fullscreen pass, one blit, and an `RGBA16F` buffer per viewport.
|
||||
- OCIO dynamic properties (exposure/gamma) and explicit look overrides are not
|
||||
wired up.
|
||||
- Logic lives in `UsdSceneRenderer.cpp` rather than its own module, by D5.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Keep `HdxColorCorrectionTask`, fix configuration.** Rejected: the failure
|
||||
was inside the bundled USD's GPU/LUT integration, not our parameters; we
|
||||
cannot patch it, and sRGB-correct/OCIO-black pointed at the hdx shader path.
|
||||
- **Custom sRGB only, keep hdx for OCIO.** Rejected: OCIO is the actual
|
||||
requirement; a custom sRGB-only path wouldn't address it.
|
||||
- **Bake the OCIO transform to a single 3D LUT on the CPU.** Rejected: loses
|
||||
HDR shaper handling and precision that `GpuShaderDesc` manages correctly.
|
||||
- **New `ViewportColorCorrection.{h,cpp}` module.** Rejected for now (D5) to
|
||||
avoid a CMake reconfigure / Cycles rebuild; revisit if the file grows.
|
||||
|
||||
## Verification
|
||||
|
||||
Built clean (Release) and confirmed in-app by the maintainer: OCIO produces the
|
||||
expected corrected (non-black) image and sRGB is unchanged. Headless automated
|
||||
verification is blocked by the stale test-target CMake config (missing USD
|
||||
include dirs); restoring those targets would let an OCIO-vs-sRGB center-pixel
|
||||
test assert this automatically.
|
||||
|
||||
## Supersession Note (2026-06-30)
|
||||
|
||||
This ADR is superseded. The custom `ViewportColorCorrector` was removed in favour
|
||||
of the approach used in usdview's `stageView.py`: `colorCorrectionMode`,
|
||||
`ocioDisplay`, `ocioView`, `ocioColorSpace`, and `ocioLook` are now passed
|
||||
directly to `UsdImagingGLRenderParams` and `SetColorCorrectionSettings()`,
|
||||
delegating correction to Hydra's `HdxColorCorrectionTask`. The two failure modes
|
||||
documented in D1 (black OCIO output, bind-stack corruption) may resurface; the
|
||||
try/catch guard around `Render()` remains in place to mitigate the bind-stack
|
||||
issue.
|
||||
@@ -0,0 +1,315 @@
|
||||
# ADR 0002 — Render Layer 面板(Maya 風格,建構於 USD Sublayer 架構之上)
|
||||
|
||||
- **Status:** Proposed(尚未實作)
|
||||
- **Date:** 2026-07-12
|
||||
- **Component:** `src/core/RenderLayerManager`(新)、`src/ui/RenderLayerPanel`(新)、
|
||||
`src/core/commands/RenderLayerCommands`(新)、
|
||||
`src/core/commands/ConnectShaderAttrsCommand`、
|
||||
`src/core/commands/DisconnectShaderAttrCommand`、`src/ui/MaterialEditorPanel`、
|
||||
`src/core/UsdStageManager`、`src/ui/StageEditorPanel`、
|
||||
`src/ui/SceneHierarchyPanel`、`src/ui/Application`
|
||||
- **Commit:** (尚未實作,無對應 commit)
|
||||
|
||||
## Context
|
||||
|
||||
使用者希望在本應用中加入一個「Render Layer」面板,行為比照 Maya 的
|
||||
Render Layers:使用者可以把場景中的燈光與可渲染物件加入某個具名圖層,
|
||||
該圖層記錄對這些物件的屬性調整——包含 shading 連線的修改——而不影響
|
||||
場景的基礎資料;切換目前作用中的圖層時,viewport 套用該圖層的所有覆寫,
|
||||
並且**只顯示該圖層的成員燈光與物件**;預設圖層(Default)則等同於目前
|
||||
未經任何 render layer 修改的 base stage。
|
||||
|
||||
為了先弄清楚該怎麼設計,再動手實作,我們先派出三個平行的研究/探索
|
||||
subagent:一個研究 Maya Render Layers(含舊版 Render Layers 與後續的
|
||||
Render Setup)的實際資料模型與行為語意;一個深入探索本專案既有的
|
||||
layer/stage/command 架構(`LayerManager`、`UsdStageManager`、
|
||||
`CommandHistory`、既有的 command 範本、`PropertyManager`、
|
||||
`StageEditorPanel`);一個探索 viewport 可見度控制與既有的材質覆寫機制
|
||||
(`UsdSceneRenderer`/`UsdImagingGLEngine`、`SceneHierarchyPanel` 的
|
||||
眼睛圖示隱藏機制、`MaterialEditorPanel` 的材質綁定與 shader 連線
|
||||
command)。三份研究都完成後,再交由一個 Plan agent 讀取實際原始碼,
|
||||
驗證並修正整體設計——這個過程中抓到兩個既有程式碼裡的真實正確性問題
|
||||
(見下方 D3、D4)。本 ADR 記錄這輪研究與規劃後定案的設計決策。
|
||||
|
||||
**與真實 Maya 行為的刻意偏離:** Maya 切換 render layer 預設**不會**
|
||||
自動 isolate viewport——那只是純粹的 render-time/override-time 概念,
|
||||
非成員物件只有在該圖層明確加了 visibility override 時才會被隱藏。
|
||||
但本專案的使用者明確要求「切換到圖層時,只顯示該圖層內的燈光與物件」,
|
||||
這是經與使用者確認過的刻意設計,而非誤解 Maya 行為。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 非破壞性地記錄每個圖層的屬性覆寫(未來階段含 shading connection 與
|
||||
material binding 覆寫),不修改場景的基礎資料。
|
||||
- 切換作用中圖層時,viewport 即時反映該圖層的所有覆寫。
|
||||
- 切換作用中圖層時,viewport 只顯示該圖層的成員燈光/物件(isolate)。
|
||||
- 正確的存檔/重新開檔 round-trip(圖層清單、成員資格、覆寫內容、目前
|
||||
作用中的圖層都要能還原)。
|
||||
- 圖層切換與圖層內的編輯都要能正確 Undo/Redo。
|
||||
|
||||
**Non-Goals(本階段,MVP 範圍之外,留待後續 Phase):**
|
||||
- Shading connection 覆寫的使用者介面(底層機制在 Phase 0 就會修正到
|
||||
可正確運作,但本階段不在 Render Layer 面板上開放這個操作)。
|
||||
- Material binding(材質指派)覆寫的使用者介面。
|
||||
- 每圖層覆寫清單的檢視/還原(revert)UI,比照 Maya Render Setup 的
|
||||
Property Editor 覆寫堆疊視圖。
|
||||
- 從 Scene Hierarchy 拖放物件到 Render Layer 面板做成員編輯(MVP 先用
|
||||
「加入所選」按鈕取代)。
|
||||
|
||||
這些項目會在後續 Phase(見 Consequences 與另一份實作計畫
|
||||
`misty-watching-stardust.md`)補齊,此處先列出以留紀錄,避免日後誤以為
|
||||
被遺漏。
|
||||
|
||||
## Decision
|
||||
|
||||
### D1 — 每個 Render Layer 對應一個獨立、檔案化的 `SdfLayer`
|
||||
|
||||
**決策:** 每建立一個 render layer,就建立一個實體(檔案化,而非
|
||||
anonymous)的 `SdfLayer`,掛載為 stage root layer 的 sublayer,並以
|
||||
`SdfLayer::SetCustomLayerData` 標記
|
||||
(例如 `usdLayerManager:renderLayer = true`、
|
||||
`usdLayerManager:renderLayerName = "<名稱>"`),藉此與一般內容 sublayer
|
||||
區分。絕對不可放進 session layer。
|
||||
|
||||
**理由:** `UsdStageManager::MergeSessionLayerIntoRoot()` 會在每次存檔
|
||||
時把 session layer 的內容攤平合併進 root layer、再清空 session
|
||||
layer——若 render layer 的資料放在 session layer,存檔當下就會被摧毀。
|
||||
另外,anonymous `SdfLayer` 的 identifier(`anon:0x...`)只在執行期有效,
|
||||
寫進 `subLayerPaths` 之後無法在 `SaveStageAs`/重新開檔時被正確解析,
|
||||
因此每個 render layer 必須是真正的檔案(沿用
|
||||
`LayerManager::CreateNewSublayer` 既有的「建立檔案 → 存檔 → 插入
|
||||
sublayer」流程)。
|
||||
|
||||
---
|
||||
|
||||
### D2 — Default 圖層是虛擬的,沒有對應的 `SdfLayer`
|
||||
|
||||
**決策:** 「Default」不是一個真正的 render layer 物件,選取它只代表
|
||||
「把所有 render-layer sublayer 全部靜音(mute)」。
|
||||
|
||||
**理由:** 這正好等同於今天應用程式既有的行為——沒有任何 render layer
|
||||
生效時,viewport 顯示的就是未經修改的 base stage。不需要為 Default
|
||||
另外設計一套資料結構或程式碼路徑。
|
||||
|
||||
---
|
||||
|
||||
### D3 — 成員資格以 `UsdCollectionAPI` 表示,並修正「非現用圖層無法讀寫」的問題
|
||||
|
||||
**決策:** 每個 render layer 的成員資格,用 `UsdCollectionAPI`
|
||||
(instance name 固定為 `"members"`)表示,掛在該圖層自己 `SdfLayer`
|
||||
內部一個固定保留路徑(`/RenderLayerData`)的 `over`-only admin prim
|
||||
上。這個 prim 只有 `over`、沒有任何 `def`,所以是合法、可定址的
|
||||
`UsdPrim`,但 `IsDefined() == false`,不會出現在
|
||||
`UsdStage::Traverse()`、Scene Hierarchy 面板、或
|
||||
`PropertyManager::GetPrimPaths()` 的預設列舉結果中,不需要額外過濾。
|
||||
|
||||
規劃過程中發現一個關鍵問題:由於同一時間只有一個 render layer 是
|
||||
「現用」(unmuted)的,其餘全部處於 muted 狀態,而
|
||||
`UsdStage::MuteLayer` 會讓該圖層**完全**退出 stage 的合成
|
||||
(composition)——也就是說,對一個目前被 mute 的圖層呼叫
|
||||
`stage->GetPrimAtPath("/RenderLayerData")` 會拿到一個**無效**的
|
||||
`UsdPrim`,`Usd` 層級的 `UsdCollectionAPI` 便完全無法讀寫該圖層的成員
|
||||
資料(例如使用者想在 Render Layer 面板編輯一個目前非現用圖層的成員
|
||||
清單時)。
|
||||
|
||||
**修正做法:** 對於非現用圖層的成員資格讀寫,一律改走**Sdf 層級 API**,
|
||||
直接對該圖層的 `SdfLayerHandle` 操作(`SdfCreatePrimInLayer`
|
||||
建立/取得 `/RenderLayerData` 的 prim spec,再用 `SdfRelationshipSpec`
|
||||
操作 `collection:members:includes` 這個 relationship 的
|
||||
target path 清單)——這條路徑完全不經過 stage 合成,因此不受 mute
|
||||
狀態影響,而且寫入的 opinion 名稱與 `UsdCollectionAPI` 完全一致。
|
||||
只有在計算「目前現用圖層」的 isolate 用成員集合時,才使用一般
|
||||
`Usd` 層級 API(`UsdCollectionAPI::GetCollection` +
|
||||
`ComputeMembershipQuery` + `UsdComputeIncludedObjectsFromCollection`),
|
||||
因為此時該圖層必然已經是 unmuted 的。兩條路徑存取的是同一份
|
||||
authored opinion,只是兩種不同的入口,不是兩套 schema。
|
||||
|
||||
---
|
||||
|
||||
### D4 — 重用既有的 edit-target 機制做屬性覆寫,並修正三個既有的正確性缺陷
|
||||
|
||||
**決策:** 切換作用中的 render layer 時,把 stage 的
|
||||
ambient edit target 指向該圖層的 `SdfLayer`(沿用
|
||||
`LayerManager::SetEditTarget`)。
|
||||
|
||||
**理由與驗證:** 重新閱讀 `PropertyManager.cpp` 與 `TransformCommand`
|
||||
後確認,`PropertyManager::SetPropertyValue`(Property Panel 一般屬性
|
||||
編輯的路徑)與 `TransformCommand`(viewport 操作桿與 Property Panel
|
||||
的 Transform 編輯路徑)都是在編輯提交的當下,從 stage 目前的
|
||||
ambient edit target 解析要寫入哪個 `SdfLayer`。因此,只要把 ambient
|
||||
edit target 換成 render layer 的 `SdfLayer`,這兩條既有的寫入路徑
|
||||
**完全不需要修改任何程式碼**,就會自動把編輯正確記錄到對應的 render
|
||||
layer 裡。
|
||||
|
||||
但在驗證過程中,發現另外三個既有的寫入路徑目前**沒有**捕捉/套用
|
||||
`UsdEditContext`,而是在 `Execute()`/`Undo()` 真正執行的當下才讀取
|
||||
ambient edit target:`ConnectShaderAttrsCommand`、
|
||||
`DisconnectShaderAttrCommand`、以及
|
||||
`MaterialEditorPanel::BindMaterialToTarget` 的材質綁定/解除綁定
|
||||
closure。具體會出錯的情境:使用者在 Render Layer A 生效時連接了一條
|
||||
shader 連線(此時正確寫入 A);接著切換到 Render Layer B(ambient
|
||||
edit target 也隨之換成 B);此時按下復原(Undo),`Undo()` 會在
|
||||
**B** 這個圖層上執行「移除連線」,而不是回到原本執行 `Execute()`
|
||||
時的 A——結果是 A 留下一條沒被正確復原的連線 opinion,B 卻多了一條
|
||||
不該存在的覆寫。這其實是既有程式碼中已經存在、但目前很少被觸發到的
|
||||
潛在 bug(任何在「連線」與「復原連線」之間發生 edit target 變更的
|
||||
情境都會中招),必須在導入 render layer 之前先修正,否則問題會被
|
||||
render layer 的切換行為放大成使用者能輕易踩到的日常錯誤。
|
||||
|
||||
**修正做法:** 比照 `TransformCommand` 既有的寫法,為
|
||||
`ConnectShaderAttrsCommand`、`DisconnectShaderAttrCommand`
|
||||
新增一個建構參數 `SdfLayerHandle editLayer`,並在 `Execute()`/
|
||||
`Undo()` 內以 `UsdEditContext(m_stage, m_editLayer)`
|
||||
包裹實際的連線/解除連線呼叫;`MaterialEditorPanel` 在建立這些
|
||||
command 與綁定/解除綁定的 closure 時,一併在建構當下捕捉
|
||||
`stage->GetEditTarget().GetLayer()` 並傳入。這個修正不影響現有
|
||||
(尚無 render layer 時)的行為——沒有明確傳入圖層時,維持原本讀取
|
||||
ambient edit target 的行為不變。
|
||||
|
||||
---
|
||||
|
||||
### D5 — Viewport Isolate 採用「authored visibility opinion」,而非重建渲染引擎或整個 stage 的 population mask
|
||||
|
||||
**決策:** 圖層啟用、或該圖層成員資格變動時,對場景做一次由上而下的
|
||||
遍歷:若某個子樹完全不含任何成員,就在該子樹**最上層**的 prim 上,
|
||||
用 `UsdGeomImageable`(與 Scene Hierarchy 面板既有的眼睛圖示隱藏
|
||||
機制完全相同的原語)authoring 一個 `visibility = invisible`,並且
|
||||
**停止往下遞迴**(子孫節點靠繼承取得隱藏效果);若子樹內含成員,則不
|
||||
寫入任何東西、繼續往下遞迴。規則上永遠不主動 authoring
|
||||
`visible`——只利用預設的「inherited」語意,避免蓋掉場景中原本就刻意
|
||||
隱藏的物件。所有自動寫入的路徑,記錄在該圖層自己的
|
||||
`customLayerData`(例如 `usdLayerManager:autoHiddenPaths`)中,
|
||||
以便下次重新計算前可以精準只清除這些自動產生的 opinion,不誤刪
|
||||
使用者手動authoring 的覆寫。
|
||||
|
||||
**理由(排除的替代方案見下方 Alternatives Considered):**
|
||||
重新閱讀
|
||||
`third_party/OpenUSD-v25.05/include/pxr/usdImagingGL/engine.h`
|
||||
確認,`UsdImagingGLEngine::Parameters::invisedPaths`
|
||||
只能在**建構時**指定,整個類別沒有任何執行期可用的
|
||||
setter——每次切換圖層都重建 engine 的代價太高(會丟失 Hydra
|
||||
scene-index 狀態,以及 Arnold/Cycles/Embree 等漸進式渲染委派已累積
|
||||
的取樣結果,並造成 viewport 明顯的畫面閃爍)。而
|
||||
`UsdStage::SetPopulationMask()`/`SetLoadRules()`
|
||||
雖然可在執行期呼叫,但作用範圍是整個 stage 的合成,會連帶讓
|
||||
Scene Hierarchy 面板、Property Panel 的選取、viewport 的
|
||||
pick 都看不到/選不到非成員物件——這遠超出「只讓 viewport
|
||||
isolate」的需求,等於把物件從整個場景圖裡「拿掉」而非「隱藏」。
|
||||
以真正的 authored opinion 達成 isolate,則是可檢視、可除錯的
|
||||
USD 資料(能在 Stage Editor 等既有工具中直接看到),而且已經證實
|
||||
Hydra 能對這類編輯即時反應而不需要重建 engine(既有的 Property
|
||||
Panel 編輯、Scene Hierarchy 的眼睛圖示隱藏,都是同一套機制)。
|
||||
開銷與被隱藏的「子樹根節點」數量成正比,而非與場景總物件數成正比,
|
||||
對一般場景規模而言可忽略。
|
||||
|
||||
---
|
||||
|
||||
### D6 — 圖層切換本身也是一個可 Undo 的操作
|
||||
|
||||
**決策:** 新增 `RenderLayerActivateCommand`,和其他編輯一樣推入
|
||||
`CommandHistory`。建構時就先記錄「切換前的 edit target」,以便
|
||||
Undo 或切回 Default 時正確還原。
|
||||
|
||||
**理由:** 切換圖層會 authoring 真正的狀態(isolate 用的 visibility
|
||||
opinion、edit target 的變更),如果不納入 Undo 堆疊,使用者連續按
|
||||
Ctrl+Z 時會「跳過」圖層切換這一步,卻仍然復原了在該圖層內做的編輯,
|
||||
造成不一致、難以理解的復原歷史。此設計已與使用者確認。
|
||||
|
||||
---
|
||||
|
||||
### D7 — 圖層生效期間,鎖定既有的 edit-target 相關 UI
|
||||
|
||||
**決策:** 當某個非 Default 的 render layer 是現用狀態時,停用
|
||||
Stage Editor 面板的 edit-target checkbox,以及 Scene Hierarchy
|
||||
面板的圖層下拉選單,並加上提示文字說明原因。
|
||||
|
||||
**理由:** 這兩個既有的 UI 都是直接讀寫 stage 的 ambient edit
|
||||
target。如果放任使用者在 render layer 生效期間透過這些既有 UI
|
||||
把 edit target 改到別的地方,「編輯會正確落在目前作用中的 render
|
||||
layer」這個核心不變量就會被悄悄破壞,且沒有任何提示。鎖定雖然犧牲
|
||||
一些彈性,但能避免使用者在不知情的狀況下把覆寫寫錯地方。此設計已
|
||||
與使用者確認(相對於「保留可操作、只顯示警告」的替代方案)。
|
||||
|
||||
---
|
||||
|
||||
### D8 — 存檔與重新開檔的持久化
|
||||
|
||||
**決策:** `UsdStageManager::SaveStage`/`SaveStageAs`
|
||||
除了既有的存檔呼叫之外,額外明確逐一存下**每一個** render-layer
|
||||
`SdfLayer`(不論目前是否被 mute)。哪個圖層目前是「現用」這件事,
|
||||
額外以 root layer 的 custom layer data 記錄
|
||||
(例如 `usdLayerManager:activeRenderLayer`),並在
|
||||
`Application::RefreshManagers()`/開啟 stage 時讀回,重新套用對應的
|
||||
`MuteLayer`/`UnmuteLayer` 與 `SetEditTarget`。
|
||||
|
||||
**理由:** `UsdStage::Save()`
|
||||
只會走訪目前在合成(composition)中的 layer 存檔,而同一時間必然有
|
||||
N-1 個 render-layer sublayer 處於 muted、不在合成中的狀態——若不
|
||||
額外處理,這些圖層的編輯內容在存檔時會被靜默遺漏。另外,mute 狀態
|
||||
本身只是 `UsdStage` 的執行期旗標,並不會被序列化進任何檔案,因此
|
||||
「目前是哪個圖層生效」這件事也必須另外顯式持久化,否則重新開檔後
|
||||
會遺失,退回成 Default 生效的狀態。
|
||||
|
||||
## Consequences
|
||||
|
||||
**正面:**
|
||||
- 幾乎完全重用既有的 `SdfLayer`/`UsdEditContext`/`CommandHistory`/
|
||||
`LayerManager` 機制,不需要引入新的渲染或合成機制,實作與既有
|
||||
程式碼風格高度一致。
|
||||
- 每個圖層的覆寫都是真正、可檢視的 USD opinion,能在 Stage Editor
|
||||
等既有工具中直接檢視、除錯,不是一個只存在於應用程式記憶體裡的
|
||||
「影子」狀態。
|
||||
- Isolate 邏輯的開銷與「被隱藏的子樹根節點數」成正比,不隨場景總
|
||||
物件數線性增加。
|
||||
- 圖層切換、圖層內編輯,都能正確納入既有的 Undo/Redo 體系,使用者
|
||||
操作心智模型一致。
|
||||
|
||||
**負面/取捨:**
|
||||
- 成員資格需要維護兩條存取路徑(現用圖層走 `Usd` 層級 API、非現用
|
||||
圖層走 `Sdf` 層級 API),增加一些實作複雜度與日後維護成本。
|
||||
- 鎖定既有 edit-target UI 犧牲了一部分操作彈性。
|
||||
- 每個 render layer 都對應一個實體檔案(而非 anonymous layer),
|
||||
會在專案目錄下產生額外的附屬檔案,需要一致的檔名/存放規則。
|
||||
- 本階段(MVP)尚未支援 shading connection、material binding 的
|
||||
每圖層覆寫 UI,也還沒有覆寫清單的檢視/還原介面——這些留待後續
|
||||
Phase,在此之前 Render Layer 面板的覆寫能力僅限一般屬性數值與
|
||||
Transform。
|
||||
- `ConnectShaderAttrsCommand`/`DisconnectShaderAttrCommand`/
|
||||
`BindMaterialToTarget` 的修正屬於既有程式碼的行為調整,雖然設計上
|
||||
刻意保持向下相容(未傳入圖層時行為不變),仍需要在導入 render
|
||||
layer 之前完成並個別驗證,不能與 render layer 本身的功能驗證
|
||||
混在一起。
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **用 `UsdStage::SetPopulationMask()`/`SetLoadRules()` 做 isolate。**
|
||||
否決:這兩者作用在整個 stage 的合成層級,會讓 Scene Hierarchy
|
||||
面板、Property Panel 的選取與 viewport 的 pick 一併看不到/選不到
|
||||
非成員物件,遠超出「只讓 viewport isolate」的實際需求。
|
||||
- **用 `UsdImagingGLEngine` 建構期的 `invisedPaths`,每次切換圖層就
|
||||
重建 engine。** 否決:代價過高——會丟失 Hydra scene-index 狀態,
|
||||
以及漸進式渲染委派(Arnold/Cycles/Embree)已累積的取樣結果,並造成
|
||||
viewport 明顯閃爍,不適合互動式的頻繁圖層切換。
|
||||
- **用 anonymous `SdfLayer` 存放每個 render layer 的資料。**
|
||||
否決:anonymous layer 的 identifier 只在執行期有效,寫進
|
||||
`subLayerPaths` 後,在 `SaveStageAs`/重新開檔時無法被正確解析,
|
||||
資料會在存檔/重載之間遺失。
|
||||
- **用 `UsdVariantSet` 表示 render layer 之間互斥的選擇。**
|
||||
否決:Maya 的 collection + override 模型本質上更接近可疊加、
|
||||
可獨立排序的 sublayer/override 合成,而非彼此互斥、一次只能選一
|
||||
個的 variant 切換,語意上不夠貼合(尤其考慮到未來允許同一物件
|
||||
同時屬於多個圖層的情境)。
|
||||
|
||||
## Verification
|
||||
|
||||
本 ADR 記錄的是**實作前**的設計決策——目前尚未進行任何程式碼變更,
|
||||
因此本節暫不回報建置或手動驗證結果。待對應的實作計畫
|
||||
(`misty-watching-stardust.md` 中記錄的 Phase 0/Phase 1 MVP)實際
|
||||
執行完成後,應在本節或另外新增一則後續紀錄中,補上實際的建置結果與
|
||||
手動驗證步驟/結果(圖層建立與切換、viewport isolate 是否正確、
|
||||
屬性覆寫是否正確落在對應圖層且不外洩到其他圖層、存檔重新開檔的
|
||||
round-trip、跨圖層切換的 Undo/Redo 行為),寫法比照
|
||||
`0001-viewport-color-correction.md` 文末的「Supersession Note」
|
||||
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
|
||||
決策當下的紀錄。
|
||||
|
After Width: | Height: | Size: 609 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 518 KiB |
@@ -1,33 +0,0 @@
|
||||
[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
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
## 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`
|
||||
- [x] 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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 5.15.4 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 2021 Fonticons, Inc. --><path d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.9-32 40.971-16.971l80 80c8.994 8.994 9.055 23.508 0 32.781l-80 80C409.956 271.748 384 261.641 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.591-25.993-31.806-40.971-16.971l-80 80c-9.055 9.273-8.994 23.787 0 32.781l80 80C102.1 463.93 128 453.686 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"/></svg>
|
||||
|
After Width: | Height: | Size: 680 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="M41.4 233.4c12.5-12.5 32.8-12.5 45.3 0L256 402.7 425.4 233.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0l-192-192c-12.5-12.5-12.5-32.8 0-45.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 471 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="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
@@ -1 +1,39 @@
|
||||
<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>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="eye-slash.svg"
|
||||
width="512"
|
||||
height="512"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.7441406"
|
||||
inkscape:cx="319.92833"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<!--! 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="M 45.154688,63.56677 C 37.354061,57.416274 26.02815,58.841389 19.877654,66.642017 13.727159,74.442646 15.152273,85.768558 22.952903,91.919053 L 466.98865,439.94706 c 7.80061,6.1505 19.12653,4.72539 25.27703,-3.07524 6.15049,-7.80063 4.72538,-19.12654 -3.07524,-25.27704 l -78.90636,-61.80497 c 29.7024,-30.45246 49.80401,-64.5802 59.92982,-88.80716 2.4752,-5.92547 2.4752,-12.526 0,-18.45147 -11.17589,-26.77716 -34.65278,-65.7803 -69.75561,-98.33292 -35.25284,-32.85264 -83.78174,-60.454866 -144.38662,-60.454866 -51.15412,0 -93.75756,19.726586 -126.98523,45.603666 z M 183.39082,171.87549 c 19.12653,-17.47641 44.70359,-28.12727 72.68085,-28.12727 59.6298,0 108.0087,48.37889 108.0087,108.0087 0,18.6765 -4.72539,36.22791 -13.05107,51.52914 l -28.95232,-22.65182 c 6.30051,-14.47616 7.95063,-31.05249 3.60029,-47.47882 -8.32567,-31.1275 -35.85288,-52.05419 -66.45536,-53.3293 -4.35034,-0.15001 -6.90055,4.57537 -5.55044,8.77572 1.57513,4.80038 2.4752,9.90078 2.4752,15.22622 0,7.65061 -1.80014,14.85119 -4.9504,21.22671 L 183.46581,171.95049 Z M 295.82487,352.19 c -12.30099,4.87539 -25.72707,7.57562 -39.7532,7.57562 -59.62981,0 -108.0087,-48.3789 -108.0087,-108.0087 0,-5.17542 0.37503,-10.20082 1.05009,-15.15122 L 78.382365,180.87621 c -17.101377,22.27679 -29.327363,44.47858 -36.452936,61.65497 -2.475199,5.92547 -2.475199,12.526 0,18.45147 11.175899,26.77717 34.652789,65.7803 69.755611,98.33293 35.25284,32.85264 83.78174,60.45487 144.38663,60.45487 35.85289,0 67.43042,-9.67578 94.65761,-24.37697 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.75006" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1 +1,39 @@
|
||||
<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>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="eye.svg"
|
||||
width="512"
|
||||
height="512"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.7441406"
|
||||
inkscape:cx="287.82083"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<!--! 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="m 250.94921,80.734602 c -63.04963,0 -113.53614,28.715668 -150.28908,62.893558 -36.518844,33.86576 -60.942769,74.44226 -72.491463,102.29957 -2.575046,6.16451 -2.575046,13.0313 0,19.1958 11.548694,27.85732 35.972619,68.43382 72.491463,102.29958 36.75294,34.17789 87.23945,62.89356 150.28908,62.89356 63.04962,0 113.53613,-28.71567 150.28907,-62.89356 36.51884,-33.94379 60.94277,-74.44226 72.56949,-102.29958 2.57505,-6.1645 2.57505,-13.03129 0,-19.1958 C 462.18105,218.07042 437.75712,177.49392 401.23828,143.62816 364.48534,109.45027 313.99883,80.734602 250.94921,80.734602 Z M 138.58355,255.52563 a 112.36566,112.36566 0 1 1 224.73132,0 112.36566,112.36566 0 1 1 -224.73132,0 z m 112.36566,-49.94029 c 0,27.54519 -22.39511,49.94029 -49.9403,49.94029 -5.54025,0 -10.84641,-0.93637 -15.84043,-2.57505 -4.29175,-1.40456 -9.28578,1.24851 -9.12972,5.77436 0.2341,5.38418 1.01441,10.76837 2.49702,16.15256 10.69034,39.95223 51.81305,63.67387 91.76529,52.98353 39.95224,-10.69034 63.67387,-51.81305 52.98353,-91.76529 -8.66152,-32.38316 -37.29916,-54.15401 -69.13609,-55.48055 -4.52584,-0.15607 -7.17892,4.75993 -5.77435,9.12971 1.63866,4.99403 2.57505,10.30019 2.57505,15.84044 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.780317" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 863 B After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="file-plus.svg"
|
||||
width="64"
|
||||
height="64"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="13.953125"
|
||||
inkscape:cx="30.351624"
|
||||
inkscape:cy="32.788354"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<!--! 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="m 18.366884,5.8051512 c -3.568558,0 -6.469907,2.9085663 -6.469907,6.4860018 v 38.916014 c 0,3.577435 2.901349,6.486002 6.469907,6.486002 h 25.879632 c 3.568559,0 6.469908,-2.908567 6.469908,-6.486002 V 22.020157 H 37.776609 c -1.789335,0 -3.234954,-1.449217 -3.234954,-3.243002 V 5.8051512 Z m 19.409725,0 V 18.777155 H 50.716424 Z M 33.732916,29.316909 v 4.864502 h 4.852431 c 1.344528,0 2.426216,1.084379 2.426216,2.43225 0,1.347873 -1.081688,2.432252 -2.426216,2.432252 h -4.852431 v 4.864501 c 0,1.347872 -1.081688,2.43225 -2.426216,2.43225 -1.344527,0 -2.426215,-1.084378 -2.426215,-2.43225 v -4.864501 h -4.852431 c -1.344528,0 -2.426215,-1.084379 -2.426215,-2.432252 0,-1.347871 1.081687,-2.43225 2.426215,-2.43225 h 4.852431 v -4.864502 c 0,-1.347872 1.081688,-2.43225 2.426215,-2.43225 1.344528,0 2.426216,1.084378 2.426216,2.43225 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.101217" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill="white" d="M495.9 166.6c3.2 8.7.5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8C283.7 510.8 269.5 512 255 512s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4l-55.7 17.7c-8.8 2.8-18.6.3-24.5-6.8-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1C25.1 368.8 19.8 357.4 15.4 345.5c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.0 273.1 63.4 264.6 63.4 256s.6-17.1 1.7-25.4L21.8 191.2c-6.9-6.2-9.6-15.9-6.4-24.6 4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2 5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1C196.5 12.3 203.5 5.1 212.7 3.6 226.5 1.2 240.7 0 255.2 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8 8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM255.2 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 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="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192v-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448h160c88.4 0 160-71.6 160-160z"/></svg>
|
||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 60 60 L 200 60 L 200 452 L 60 452 Z M 312 60 L 452 60 L 452 452 L 312 452 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 160 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="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 552 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 472 60 L 472 452 L 40 256 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 112 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 40 60 L 40 452 L 472 256 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 111 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 40 60 L 115 60 L 115 452 L 40 452 Z M 460 60 L 460 452 L 155 256 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 40 60 L 362 256 L 40 452 Z M 402 60 L 472 60 L 472 452 L 402 452 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 256 60 L 256 452 L 40 256 Z M 472 60 L 472 452 L 256 256 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 143 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M 40 60 L 256 256 L 40 452 Z M 256 60 L 472 256 L 256 452 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 142 B |
@@ -1 +1,37 @@
|
||||
<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>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="swatchbook.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.7441406"
|
||||
inkscape:cx="255.71333"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<!--! 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="m 36.12094,73.137738 c 0,-14.449385 12.282308,-26.12318 27.484883,-26.12318 H 173.54535 c 15.20258,0 27.48489,11.673795 27.48489,26.12318 V 386.6159 c 0,43.26652 -36.93281,78.36954 -82.45465,78.36954 -45.521839,0 -82.45465,-35.10302 -82.45465,-78.36954 z M 228.17156,394.69776 c 0.25766,-2.69395 0.34356,-5.38791 0.34356,-8.08186 V 172.73237 l 64.76125,-61.55275 c 10.73629,-10.20437 28.172,-10.20437 38.90828,0 l 77.7307,73.87962 c 10.73629,10.20436 10.73629,26.77626 0,36.98062 z m -35.04323,70.28768 164.9093,-156.73908 h 90.35655 c 15.20257,0 27.48488,11.6738 27.48488,26.12318 v 104.49273 c 0,14.44938 -12.28231,26.12317 -27.48488,26.12317 z M 146.06047,99.260915 H 91.090705 V 151.50728 H 146.06047 Z M 91.090705,203.75364 V 256 h 54.969765 v -52.24636 z m 27.484885,202.45465 a 20.613662,19.592386 0 1 0 0,-39.18477 20.613662,19.592386 0 1 0 0,39.18477 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.837356" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 693 B After Width: | Height: | Size: 2.1 KiB |
@@ -148,6 +148,33 @@ void LayerManager::UnmuteLayer(const std::string& layerIdentifier) {
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerManager::SetEditTarget(const std::string& identifier) {
|
||||
if (!m_stage) return false;
|
||||
SdfLayerHandle layer = SdfLayer::Find(identifier);
|
||||
if (!layer) return false;
|
||||
m_stage->SetEditTarget(layer);
|
||||
Refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerManager::CreateNewSublayer(const std::string& absolutePath) {
|
||||
if (!m_stage) return false;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
|
||||
auto newLayer = SdfLayer::CreateNew(absolutePath);
|
||||
if (!newLayer) {
|
||||
LOG_ERROR("Failed to create layer file: " + absolutePath);
|
||||
return false;
|
||||
}
|
||||
newLayer->Save();
|
||||
|
||||
rootLayer->InsertSubLayerPath(absolutePath, 0);
|
||||
m_stage->SetEditTarget(newLayer);
|
||||
Refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerManager::IsLayerMuted(const std::string& layerIdentifier) const {
|
||||
if (m_stage) {
|
||||
return m_stage->IsLayerMuted(layerIdentifier);
|
||||
@@ -181,28 +208,47 @@ std::string LayerManager::ExtractDisplayName(const std::string& identifier) {
|
||||
|
||||
void LayerManager::BuildLayerList() {
|
||||
m_layers.clear();
|
||||
|
||||
m_layerRefs.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 rootLayer = m_stage->GetRootLayer();
|
||||
SdfLayerHandle sessionLayer = m_stage->GetSessionLayer();
|
||||
SdfLayerHandle editTarget = m_stage->GetEditTarget().GetLayer();
|
||||
|
||||
for (const auto& layer : layerStack) {
|
||||
auto makeInfo = [&](SdfLayerHandle layer, bool isRoot, bool isSession) {
|
||||
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);
|
||||
info.layer = layer;
|
||||
info.identifier = layer->GetIdentifier();
|
||||
info.displayName = ExtractDisplayName(info.identifier);
|
||||
info.realPath = layer->GetRealPath();
|
||||
info.isMuted = m_stage->IsLayerMuted(info.identifier);
|
||||
info.isAnonymous = layer->IsAnonymous();
|
||||
info.isRootLayer = isRoot;
|
||||
info.isSessionLayer = isSession;
|
||||
info.isEditTarget = (layer == editTarget);
|
||||
info.isDirty = layer->IsDirty();
|
||||
return info;
|
||||
};
|
||||
|
||||
m_layers.push_back(info);
|
||||
// Root and session layers: stage always holds strong refs, handles are valid.
|
||||
if (sessionLayer) m_layers.push_back(makeInfo(sessionLayer, false, true));
|
||||
if (rootLayer) m_layers.push_back(makeInfo(rootLayer, true, false));
|
||||
|
||||
// Walk root layer's sublayer paths instead of stage->GetLayerStack().
|
||||
// GetLayerStack() drops muted layers because they don't contribute to
|
||||
// composition. FindOrOpenRelativeToLayer locates or reopens the layer
|
||||
// regardless of mute state. We store the returned RefPtr in m_layerRefs
|
||||
// so the layer object isn't garbage-collected before the next Refresh().
|
||||
if (rootLayer) {
|
||||
for (const auto& subPath : rootLayer->GetSubLayerPaths()) {
|
||||
SdfLayerRefPtr subRef =
|
||||
SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath);
|
||||
if (subRef) {
|
||||
m_layerRefs.push_back(subRef);
|
||||
m_layers.push_back(makeInfo(SdfLayerHandle(subRef), false, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("Built layer list with " + std::to_string(m_layers.size()) + " layers");
|
||||
|
||||
@@ -17,6 +17,8 @@ struct LayerInfo {
|
||||
bool isAnonymous;
|
||||
bool isRootLayer;
|
||||
bool isSessionLayer;
|
||||
bool isEditTarget;
|
||||
bool isDirty;
|
||||
};
|
||||
|
||||
class LayerManager {
|
||||
@@ -39,6 +41,12 @@ public:
|
||||
bool MoveSublayerUp(int index);
|
||||
bool MoveSublayerDown(int index);
|
||||
|
||||
// Edit target
|
||||
bool SetEditTarget(const std::string& identifier);
|
||||
|
||||
// Create a new USD file on disk, insert as sublayer, and activate as edit target.
|
||||
bool CreateNewSublayer(const std::string& absolutePath);
|
||||
|
||||
// Muting
|
||||
void MuteLayer(const std::string& layerIdentifier);
|
||||
void UnmuteLayer(const std::string& layerIdentifier);
|
||||
@@ -52,6 +60,8 @@ private:
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
std::vector<LayerInfo> m_layers;
|
||||
// Strong references keep sublayers alive even when the stage drops them after muting.
|
||||
std::vector<pxr::SdfLayerRefPtr> m_layerRefs;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "MaterialManager.h"
|
||||
#include <pxr/usd/sdr/registry.h>
|
||||
#include <pxr/usd/sdr/shaderProperty.h>
|
||||
#include <pxr/base/tf/stringUtils.h>
|
||||
#include <pxr/usd/usd/primRange.h>
|
||||
#include <pxr/usd/usdShade/material.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/usd/usdShade/nodeGraph.h>
|
||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||
#include <pxr/usd/usdShade/utils.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <set>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
namespace {
|
||||
|
||||
// Coarse Hypershade-style bucket for the create-node list. Sdr metadata is
|
||||
// parser-dependent (glslfx vs MaterialX vs Arnold vs Cycles fill different
|
||||
// fields), so classify from the strongest signal available: context first,
|
||||
// then terminal output types, then role, then name keywords.
|
||||
std::string DeriveCategory(pxr::SdrShaderNodeConstPtr node) {
|
||||
const std::string context = pxr::TfStringify(node->GetContext());
|
||||
if (context == "surface" || context == "volume" || context == "displacement")
|
||||
return "Material";
|
||||
if (context == "light" || context == "lightFilter")
|
||||
return "Light";
|
||||
|
||||
// MaterialX/Arnold terminals often lack a context but type their outputs
|
||||
// as surfaceshader/volumeshader/displacementshader/closure.
|
||||
for (const pxr::TfToken& outName : node->GetShaderOutputNames()) {
|
||||
if (const auto* prop = node->GetShaderOutput(outName)) {
|
||||
const std::string outType = pxr::TfStringify(prop->GetType());
|
||||
if (outType.find("shader") != std::string::npos ||
|
||||
outType == "terminal" || outType == "closure")
|
||||
return "Material";
|
||||
}
|
||||
}
|
||||
|
||||
const std::string role = pxr::TfStringify(node->GetRole());
|
||||
if (role == "texture") return "Texture";
|
||||
if (role == "primvar") return "Geometry";
|
||||
if (role == "math") return "Utility";
|
||||
|
||||
const std::string name = pxr::TfStringToLower(
|
||||
node->GetIdentifier().GetString() + " " + node->GetName());
|
||||
for (const char* kw : {"texture", "image", "checker", "noise", "ramp",
|
||||
"fractal", "worley", "voronoi", "triplanar"})
|
||||
if (name.find(kw) != std::string::npos) return "Texture";
|
||||
for (const char* kw : {"primvar", "texcoord", "geomprop", "position",
|
||||
"normal", "tangent", "bitangent", "uv_"})
|
||||
if (name.find(kw) != std::string::npos) return "Geometry";
|
||||
|
||||
return "Utility";
|
||||
}
|
||||
|
||||
// Fixed display order for the known buckets; anything unexpected sorts after.
|
||||
int CategoryRank(const std::string& category) {
|
||||
if (category == "Material") return 0;
|
||||
if (category == "Texture") return 1;
|
||||
if (category == "Geometry") return 2;
|
||||
if (category == "Utility") return 3;
|
||||
if (category == "Light") return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
// Top-level group from the Sdr source type. glslfx and OSL collapse into one
|
||||
// "USD" group — the core usd* nodes register under both parsers and would
|
||||
// otherwise show up twice under different headers.
|
||||
std::string DeriveSource(pxr::SdrShaderNodeConstPtr node) {
|
||||
std::string st = pxr::TfStringToLower(pxr::TfStringify(node->GetSourceType()));
|
||||
if (st == "glslfx" || st == "osl") return "USD";
|
||||
if (st == "mtlx") return "MaterialX";
|
||||
if (st == "arnold") return "Arnold";
|
||||
if (st == "cycles") return "Cycles";
|
||||
if (st.empty()) return "Other";
|
||||
st[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(st[0])));
|
||||
return st;
|
||||
}
|
||||
|
||||
int SourceRank(const std::string& source) {
|
||||
if (source == "USD") return 0;
|
||||
if (source == "MaterialX") return 1;
|
||||
return 2; // renderers and anything else, alphabetical among themselves
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
|
||||
if (m_shaderNodeCacheBuilt)
|
||||
return m_shaderNodeCache;
|
||||
|
||||
for (pxr::SdrShaderNodeConstPtr node : pxr::SdrRegistry::GetInstance().GetShaderNodesByFamily()) {
|
||||
if (!node) continue;
|
||||
|
||||
ShaderNodeTypeInfo info;
|
||||
info.identifier = node->GetIdentifier().GetString();
|
||||
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
|
||||
info.family = node->GetFamily().GetString();
|
||||
info.category = DeriveCategory(node);
|
||||
info.source = DeriveSource(node);
|
||||
m_shaderNodeCache.push_back(std::move(info));
|
||||
}
|
||||
|
||||
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
|
||||
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
|
||||
const int sa = SourceRank(a.source), sb = SourceRank(b.source);
|
||||
if (sa != sb) return sa < sb;
|
||||
if (a.source != b.source) return a.source < b.source;
|
||||
const int ra = CategoryRank(a.category), rb = CategoryRank(b.category);
|
||||
if (ra != rb) return ra < rb;
|
||||
if (a.category != b.category) return a.category < b.category;
|
||||
return a.label < b.label;
|
||||
});
|
||||
|
||||
m_shaderNodeCacheBuilt = true;
|
||||
return m_shaderNodeCache;
|
||||
}
|
||||
|
||||
std::vector<pxr::SdfPath> MaterialManager::GetAllMaterials() const {
|
||||
std::vector<pxr::SdfPath> result;
|
||||
if (!m_stage) return result;
|
||||
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
|
||||
if (prim.IsA<pxr::UsdShadeMaterial>())
|
||||
result.push_back(prim.GetPath());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& materialPath) const {
|
||||
ShaderGraphSnapshot snapshot;
|
||||
if (!m_stage) return snapshot;
|
||||
|
||||
pxr::UsdPrim materialPrim = m_stage->GetPrimAtPath(materialPath);
|
||||
if (!materialPrim.IsValid()) return snapshot;
|
||||
|
||||
static const pxr::TfToken kUiPositionKey("uiPosition");
|
||||
static const pxr::TfToken kUiPinDisplayModeKey("uiPinDisplayMode");
|
||||
|
||||
// Seed with every shader under the material, recursing into nested node
|
||||
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
|
||||
// scopes rather than authoring them as direct children).
|
||||
std::vector<pxr::UsdPrim> pending;
|
||||
std::vector<pxr::UsdPrim> scopes{materialPrim};
|
||||
while (!scopes.empty()) {
|
||||
pxr::UsdPrim scope = scopes.back();
|
||||
scopes.pop_back();
|
||||
for (const pxr::UsdPrim& child : scope.GetChildren()) {
|
||||
if (child.IsA<pxr::UsdShadeShader>())
|
||||
pending.push_back(child);
|
||||
else if (child.IsA<pxr::UsdShadeNodeGraph>())
|
||||
scopes.push_back(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Worklist walk: resolving each input's true upstream shader output pulls
|
||||
// in nodes living in node graphs outside the material prim too.
|
||||
std::set<pxr::SdfPath> visited;
|
||||
while (!pending.empty()) {
|
||||
pxr::UsdPrim child = pending.back();
|
||||
pending.pop_back();
|
||||
if (!visited.insert(child.GetPath()).second)
|
||||
continue;
|
||||
pxr::UsdShadeShader shader(child);
|
||||
if (!shader) continue;
|
||||
|
||||
ShaderGraphNode node;
|
||||
node.path = child.GetPath();
|
||||
|
||||
pxr::TfToken shaderId;
|
||||
shader.GetIdAttr().Get(&shaderId);
|
||||
node.shaderId = shaderId.GetString();
|
||||
|
||||
pxr::VtValue posValue = child.GetCustomDataByKey(kUiPositionKey);
|
||||
if (posValue.IsHolding<pxr::GfVec2f>()) {
|
||||
node.uiPosition = posValue.UncheckedGet<pxr::GfVec2f>();
|
||||
node.hasAuthoredPosition = true;
|
||||
}
|
||||
|
||||
pxr::VtValue pinModeValue = child.GetCustomDataByKey(kUiPinDisplayModeKey);
|
||||
if (pinModeValue.IsHolding<int>()) {
|
||||
int raw = pinModeValue.UncheckedGet<int>();
|
||||
if (raw >= static_cast<int>(PinDisplayMode::All) && raw <= static_cast<int>(PinDisplayMode::Collapsed))
|
||||
node.pinDisplayMode = static_cast<PinDisplayMode>(raw);
|
||||
}
|
||||
|
||||
// Prefer the full Sdr-defined pin set (so unauthored pins can still be
|
||||
// dragged to create a connection); fall back to authored attributes
|
||||
// only for shader types the registry doesn't know about.
|
||||
pxr::SdrShaderNodeConstPtr sdrNode =
|
||||
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(shaderId);
|
||||
if (sdrNode) {
|
||||
node.category = DeriveCategory(sdrNode);
|
||||
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
|
||||
if (auto* prop = sdrNode->GetShaderInput(inputName)) {
|
||||
ShaderPinInfo pin{inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()};
|
||||
for (const auto& option : prop->GetOptions())
|
||||
pin.allowedValues.push_back(option.first.GetString());
|
||||
node.inputs.push_back(std::move(pin));
|
||||
}
|
||||
}
|
||||
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
|
||||
if (auto* prop = sdrNode->GetShaderOutput(outputName))
|
||||
node.outputs.push_back({outputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
|
||||
}
|
||||
} else {
|
||||
for (const auto& input : shader.GetInputs())
|
||||
node.inputs.push_back({input.GetBaseName().GetString(), input.GetTypeName()});
|
||||
for (const auto& output : shader.GetOutputs())
|
||||
node.outputs.push_back({output.GetBaseName().GetString(), output.GetTypeName()});
|
||||
}
|
||||
|
||||
// Only authored inputs can carry a connection. Resolve each one
|
||||
// through node-graph boundaries (shaderOutputsOnly) so a MaterialX
|
||||
// connection routed via NodeGraph interface attrs links straight to
|
||||
// the shader output that actually produces the value.
|
||||
for (const auto& input : shader.GetInputs()) {
|
||||
for (const pxr::UsdAttribute& sourceAttr :
|
||||
pxr::UsdShadeUtils::GetValueProducingAttributes(input, /*shaderOutputsOnly=*/true)) {
|
||||
pxr::UsdPrim sourcePrim = sourceAttr.GetPrim();
|
||||
if (!sourcePrim.IsA<pxr::UsdShadeShader>()) continue;
|
||||
ShaderGraphLink link;
|
||||
link.destNode = child.GetPath();
|
||||
link.destInput = input.GetBaseName().GetString();
|
||||
link.sourceNode = sourcePrim.GetPath();
|
||||
link.sourceOutput = pxr::UsdShadeOutput(sourceAttr).GetBaseName().GetString();
|
||||
snapshot.links.push_back(std::move(link));
|
||||
pending.push_back(sourcePrim); // may live outside the material
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.nodes.push_back(std::move(node));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/valueTypeName.h>
|
||||
#include <pxr/base/gf/vec2f.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// One entry in the shader-node creation menu, derived from the Sdr/Ndr
|
||||
/// shader registry (covers UsdPreviewSurface, MaterialX, Arnold, Cycles,
|
||||
/// etc. — whatever shader definitions are discoverable in this build).
|
||||
struct ShaderNodeTypeInfo {
|
||||
std::string identifier; ///< Sdr identifier, authored as info:id
|
||||
std::string label; ///< display label for the create-node menu
|
||||
std::string family; ///< Sdr family (parser-provided, may be empty)
|
||||
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
|
||||
/// "Utility", "Light") derived from Sdr context/role/output types; the
|
||||
/// create-node list groups by it. Never empty.
|
||||
std::string category;
|
||||
/// Top-level create-list group derived from the Sdr source type:
|
||||
/// "USD" (glslfx/OSL core nodes), "MaterialX", "Arnold", "Cycles", or a
|
||||
/// capitalized raw source type. Never empty.
|
||||
std::string source;
|
||||
};
|
||||
|
||||
/// One input or output pin on a shader node, with its USD value type so
|
||||
/// connections/CreateInput/CreateOutput can author the right type.
|
||||
struct ShaderPinInfo {
|
||||
std::string name;
|
||||
pxr::SdfValueTypeName typeName;
|
||||
/// Sdr-declared allowed values (e.g. UsdUVTexture's sourceColorSpace:
|
||||
/// "raw"/"sRGB"/"auto"), empty when the property isn't enum-constrained.
|
||||
/// Drives a dropdown instead of a free-text field in the property panel.
|
||||
std::vector<std::string> allowedValues;
|
||||
};
|
||||
|
||||
/// Hypershade-style pin display mode for a node in the graph canvas.
|
||||
/// All: every Sdr-defined pin shown (today's default). Connected: only
|
||||
/// linked pins shown, the rest collapsed behind a single connectable nub per
|
||||
/// side. Collapsed: every pin — linked or not — collapses behind that one
|
||||
/// nub per side, so existing connections visually converge on it too.
|
||||
enum class PinDisplayMode {
|
||||
All = 0,
|
||||
Connected = 1,
|
||||
Collapsed = 2,
|
||||
};
|
||||
|
||||
/// One UsdShadeShader prim read back from an existing material network.
|
||||
/// inputs/outputs list the full Sdr-defined pin set (not just authored
|
||||
/// attributes) so unauthored pins can still be dragged to create a connection.
|
||||
struct ShaderGraphNode {
|
||||
pxr::SdfPath path;
|
||||
std::string shaderId;
|
||||
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
|
||||
/// "Utility", "Light") from the same classifier the create-node list uses;
|
||||
/// empty when the shader id isn't in the Sdr registry. Drives which nodes
|
||||
/// get an in-canvas thumbnail.
|
||||
std::string category;
|
||||
pxr::GfVec2f uiPosition{0.0f, 0.0f};
|
||||
/// False when no uiPosition custom data is authored (typical for networks
|
||||
/// referenced from .mtlx) — the editor auto-lays such nodes out instead.
|
||||
bool hasAuthoredPosition = false;
|
||||
/// Pin display mode, read from customData when the "keep graph node view
|
||||
/// settings in USD" preference authored it; All when unauthored.
|
||||
PinDisplayMode pinDisplayMode = PinDisplayMode::All;
|
||||
std::vector<ShaderPinInfo> inputs;
|
||||
std::vector<ShaderPinInfo> outputs;
|
||||
};
|
||||
|
||||
/// One authored connection between two shader nodes in the same network.
|
||||
struct ShaderGraphLink {
|
||||
pxr::SdfPath destNode;
|
||||
std::string destInput;
|
||||
pxr::SdfPath sourceNode;
|
||||
std::string sourceOutput;
|
||||
};
|
||||
|
||||
struct ShaderGraphSnapshot {
|
||||
std::vector<ShaderGraphNode> nodes;
|
||||
std::vector<ShaderGraphLink> links;
|
||||
};
|
||||
|
||||
/// Reads/introspects UsdShade material networks and the Sdr shader registry.
|
||||
/// Mirrors PropertyManager's role: holds read/derive logic only — mutations
|
||||
/// go through ICommand subclasses in src/core/commands/.
|
||||
class MaterialManager {
|
||||
public:
|
||||
void SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; }
|
||||
|
||||
/// All shader node types registered in the Sdr registry, grouped for a
|
||||
/// categorized create-node menu. Cached after the first (slow) call.
|
||||
const std::vector<ShaderNodeTypeInfo>& GetAvailableShaderNodes();
|
||||
|
||||
/// The entire shader network of the material at materialPath: shaders
|
||||
/// under the material (recursing into nested UsdShadeNodeGraphs) plus
|
||||
/// everything reachable upstream through connections — including nodes in
|
||||
/// node graphs outside the material, as authored by MaterialX (.mtlx)
|
||||
/// imports. Connections are resolved through node-graph boundary attrs to
|
||||
/// the shader outputs that actually produce the values.
|
||||
ShaderGraphSnapshot GetShaderGraph(const pxr::SdfPath& materialPath) const;
|
||||
|
||||
/// Paths of every UsdShadeMaterial prim on the stage, in traversal order.
|
||||
/// Drives the material browser list (Hypershade-style).
|
||||
std::vector<pxr::SdfPath> GetAllMaterials() const;
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
std::vector<ShaderNodeTypeInfo> m_shaderNodeCache;
|
||||
bool m_shaderNodeCacheBuilt = false;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "PropertyManager.h"
|
||||
#include "LayerManager.h"
|
||||
#include "CommandHistory.h"
|
||||
#include "commands/AttributeSetCommand.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/usd/primRange.h>
|
||||
@@ -123,17 +125,51 @@ bool PropertyManager::SetPropertyValue(const std::string& primPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a current layer is set, create an edit context to target it
|
||||
try {
|
||||
if (m_currentLayer) {
|
||||
UsdEditContext editCtx(m_stage, m_currentLayer);
|
||||
return ApplyValue(attr, value);
|
||||
} else {
|
||||
return ApplyValue(attr, value);
|
||||
// Capture old value for undo
|
||||
PropertyValue oldValue = ExtractValue(attr);
|
||||
SdfLayerHandle targetLayer = m_currentLayer ? m_currentLayer : m_stage->GetEditTarget().GetLayer();
|
||||
|
||||
// Create command with closures that capture old/new values and layer context
|
||||
if (m_commandHistory) {
|
||||
auto executeFunc = [this, primPath, propName, value, targetLayer]() {
|
||||
UsdPrim p = GetPrim(primPath);
|
||||
if (!p.IsValid()) return;
|
||||
UsdAttribute a = p.GetAttribute(TfToken(propName));
|
||||
if (!a.IsValid()) return;
|
||||
|
||||
UsdEditContext editCtx(m_stage, targetLayer);
|
||||
ApplyValue(a, value);
|
||||
};
|
||||
|
||||
auto undoFunc = [this, primPath, propName, oldValue, targetLayer]() {
|
||||
UsdPrim p = GetPrim(primPath);
|
||||
if (!p.IsValid()) return;
|
||||
UsdAttribute a = p.GetAttribute(TfToken(propName));
|
||||
if (!a.IsValid()) return;
|
||||
|
||||
UsdEditContext editCtx(m_stage, targetLayer);
|
||||
ApplyValue(a, oldValue);
|
||||
};
|
||||
|
||||
std::string description = "Set " + propName + " on " + primPath;
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
description, executeFunc, undoFunc
|
||||
));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Fallback: apply directly without undo support
|
||||
try {
|
||||
if (m_currentLayer) {
|
||||
UsdEditContext editCtx(m_stage, m_currentLayer);
|
||||
return ApplyValue(attr, value);
|
||||
} else {
|
||||
return ApplyValue(attr, value);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to set property: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to set property: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +183,47 @@ bool PropertyManager::SetPropertyValueInLayer(const std::string& primPath,
|
||||
UsdAttribute attr = prim.GetAttribute(TfToken(propName));
|
||||
if (!attr.IsValid()) return false;
|
||||
|
||||
try {
|
||||
UsdEditContext editCtx(m_stage, layer);
|
||||
return ApplyValue(attr, value);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to set property: ") + e.what());
|
||||
return false;
|
||||
// Capture old value for undo
|
||||
PropertyValue oldValue = ExtractValue(attr);
|
||||
|
||||
// Create command with closures that capture old/new values and layer context
|
||||
if (m_commandHistory) {
|
||||
auto executeFunc = [this, primPath, propName, value, layer]() {
|
||||
UsdPrim p = GetPrim(primPath);
|
||||
if (!p.IsValid()) return;
|
||||
UsdAttribute a = p.GetAttribute(TfToken(propName));
|
||||
if (!a.IsValid()) return;
|
||||
|
||||
UsdEditContext editCtx(m_stage, layer);
|
||||
ApplyValue(a, value);
|
||||
};
|
||||
|
||||
auto undoFunc = [this, primPath, propName, oldValue, layer]() {
|
||||
UsdPrim p = GetPrim(primPath);
|
||||
if (!p.IsValid()) return;
|
||||
UsdAttribute a = p.GetAttribute(TfToken(propName));
|
||||
if (!a.IsValid()) return;
|
||||
|
||||
UsdEditContext editCtx(m_stage, layer);
|
||||
ApplyValue(a, oldValue);
|
||||
};
|
||||
|
||||
std::string layerName = layer ? LayerManager::ExtractDisplayName(layer->GetIdentifier()) : "default";
|
||||
std::string description = "Set " + propName + " on " + primPath + " in " + layerName;
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
description, executeFunc, undoFunc
|
||||
));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Fallback: apply directly without undo support
|
||||
try {
|
||||
UsdEditContext editCtx(m_stage, layer);
|
||||
return ApplyValue(attr, value);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("Failed to set property: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
class CommandHistory;
|
||||
|
||||
using PropertyValue = std::variant<
|
||||
bool, int, float, double, std::string,
|
||||
pxr::GfVec3f, pxr::GfVec3d
|
||||
@@ -35,6 +37,7 @@ public:
|
||||
|
||||
void SetStage(UsdStageRefPtr stage);
|
||||
void SetCurrentLayer(const SdfLayerHandle& layer);
|
||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||
SdfLayerHandle GetCurrentLayer() const { return m_currentLayer; }
|
||||
|
||||
// Prim properties
|
||||
@@ -61,6 +64,7 @@ private:
|
||||
|
||||
UsdStageRefPtr m_stage;
|
||||
SdfLayerHandle m_currentLayer;
|
||||
CommandHistory* m_commandHistory = nullptr;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "RenderLayerManager.h"
|
||||
#include "LayerManager.h"
|
||||
#include "../utils/Logger.h"
|
||||
|
||||
#include <pxr/usd/usd/editContext.h>
|
||||
#include <pxr/usd/usd/editTarget.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/sdf/primSpec.h>
|
||||
#include <pxr/usd/sdf/relationshipSpec.h>
|
||||
#include <pxr/usd/sdf/proxyTypes.h>
|
||||
#include <pxr/usd/usdGeom/imageable.h>
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/base/vt/dictionary.h>
|
||||
#include <pxr/base/vt/array.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
namespace {
|
||||
// Reserved, undefined ("over"-only) admin prim path each render layer's own
|
||||
// SdfLayer carries its membership relationship on. Never a `def`, so it never
|
||||
// shows up in UsdStage::Traverse() / the Scene Hierarchy panel / GetChildren().
|
||||
const SdfPath kAdminPrimPath("/RenderLayerData");
|
||||
const TfToken kMembersIncludesName("collection:members:includes");
|
||||
|
||||
// SdfLayer custom-layer-data keys.
|
||||
const std::string kMarkerKey = "usdLayerManager:renderLayer";
|
||||
const std::string kNameKey = "usdLayerManager:renderLayerName";
|
||||
const std::string kAutoHiddenKey = "usdLayerManager:autoHiddenPaths";
|
||||
// Root-layer-only key: persists which render layer was active across save/reload.
|
||||
const std::string kActiveLayerKey = "usdLayerManager:activeRenderLayer";
|
||||
|
||||
std::string SanitizeFileStem(const std::string& raw) {
|
||||
std::string result;
|
||||
result.reserve(raw.size());
|
||||
for (char c : raw) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-')
|
||||
result += c;
|
||||
else
|
||||
result += '_';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SdfPath MembersIncludesPath() {
|
||||
return kAdminPrimPath.AppendProperty(kMembersIncludesName);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
RenderLayerManager::RenderLayerManager() {}
|
||||
RenderLayerManager::~RenderLayerManager() {}
|
||||
|
||||
void RenderLayerManager::SetStage(UsdStageRefPtr stage) {
|
||||
m_stage = stage;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void RenderLayerManager::Refresh() {
|
||||
BuildList();
|
||||
}
|
||||
|
||||
void RenderLayerManager::BuildList() {
|
||||
m_layers.clear();
|
||||
m_layerRefs.clear();
|
||||
|
||||
RenderLayerInfo def;
|
||||
def.name = "Default";
|
||||
def.isDefault = true;
|
||||
m_layers.push_back(def);
|
||||
|
||||
if (!m_stage) return;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return;
|
||||
|
||||
bool anyActive = false;
|
||||
for (const auto& subPath : rootLayer->GetSubLayerPaths()) {
|
||||
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath);
|
||||
if (!sub) continue;
|
||||
|
||||
VtDictionary cld = sub->GetCustomLayerData();
|
||||
auto markerIt = cld.find(kMarkerKey);
|
||||
if (markerIt == cld.end() || !markerIt->second.IsHolding<bool>() ||
|
||||
!markerIt->second.Get<bool>())
|
||||
continue; // not a render layer sublayer — leave for LayerManager/StageEditorPanel
|
||||
|
||||
m_layerRefs.push_back(sub);
|
||||
|
||||
RenderLayerInfo info;
|
||||
info.layer = SdfLayerHandle(sub);
|
||||
info.layerIdentifier = sub->GetIdentifier();
|
||||
auto nameIt = cld.find(kNameKey);
|
||||
info.name = (nameIt != cld.end() && nameIt->second.IsHolding<std::string>())
|
||||
? nameIt->second.Get<std::string>()
|
||||
: LayerManager::ExtractDisplayName(info.layerIdentifier);
|
||||
info.isActive = !m_stage->IsLayerMuted(info.layerIdentifier);
|
||||
if (info.isActive) anyActive = true;
|
||||
|
||||
m_layers.push_back(info);
|
||||
}
|
||||
|
||||
m_layers[0].isActive = !anyActive;
|
||||
}
|
||||
|
||||
/*static*/ bool RenderLayerManager::IsRenderLayerSublayer(const SdfLayerHandle& layer) {
|
||||
if (!layer) return false;
|
||||
VtDictionary cld = layer->GetCustomLayerData();
|
||||
auto it = cld.find(kMarkerKey);
|
||||
return it != cld.end() && it->second.IsHolding<bool>() && it->second.Get<bool>();
|
||||
}
|
||||
|
||||
void RenderLayerManager::RefreshLayerManager() {
|
||||
if (m_layerManager) m_layerManager->Refresh();
|
||||
}
|
||||
|
||||
SdfLayerHandle RenderLayerManager::FindRenderLayer(const std::string& layerId) const {
|
||||
for (const auto& info : m_layers)
|
||||
if (!info.isDefault && info.layerIdentifier == layerId)
|
||||
return info.layer;
|
||||
return SdfLayerHandle();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::string RenderLayerManager::CreateRenderLayer(const std::string& name) {
|
||||
if (!m_stage) return {};
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return {};
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
fs::path baseDir;
|
||||
std::string realPath = rootLayer->GetRealPath();
|
||||
baseDir = !realPath.empty()
|
||||
? fs::path(realPath).parent_path() / ".renderlayers"
|
||||
: fs::current_path() / ".renderlayers";
|
||||
std::error_code ec;
|
||||
fs::create_directories(baseDir, ec);
|
||||
|
||||
std::string stem = SanitizeFileStem(name.empty() ? "RenderLayer" : name);
|
||||
if (stem.empty()) stem = "RenderLayer";
|
||||
std::string finalStem = stem;
|
||||
fs::path filePath = baseDir / (finalStem + ".usda");
|
||||
int suffix = 1;
|
||||
while (fs::exists(filePath)) {
|
||||
finalStem = stem + "_" + std::to_string(suffix++);
|
||||
filePath = baseDir / (finalStem + ".usda");
|
||||
}
|
||||
|
||||
SdfLayerRefPtr newLayer = SdfLayer::CreateNew(filePath.string());
|
||||
if (!newLayer) {
|
||||
LOG_ERROR("RenderLayerManager: failed to create layer file: " + filePath.string());
|
||||
return {};
|
||||
}
|
||||
|
||||
VtDictionary cld;
|
||||
cld[kMarkerKey] = VtValue(true);
|
||||
cld[kNameKey] = VtValue(name.empty() ? finalStem : name);
|
||||
newLayer->SetCustomLayerData(cld);
|
||||
|
||||
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(newLayer, kAdminPrimPath);
|
||||
if (adminPrim)
|
||||
SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
|
||||
/*custom=*/false, SdfVariabilityUniform);
|
||||
|
||||
newLayer->Save();
|
||||
|
||||
rootLayer->InsertSubLayerPath(filePath.string(), 0);
|
||||
m_stage->MuteLayer(filePath.string()); // starts inactive until explicitly activated
|
||||
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
return newLayer->GetIdentifier();
|
||||
}
|
||||
|
||||
bool RenderLayerManager::DeleteRenderLayer(const std::string& layerId) {
|
||||
if (!m_stage) return false;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
if (!FindRenderLayer(layerId)) return false;
|
||||
|
||||
if (GetActiveRenderLayerId() == layerId)
|
||||
ActivateDefaultLayer();
|
||||
|
||||
SdfSubLayerProxy subPaths = rootLayer->GetSubLayerPaths();
|
||||
for (size_t i = 0; i < subPaths.size(); ++i) {
|
||||
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPaths[i]);
|
||||
if (sub && sub->GetIdentifier() == layerId) {
|
||||
subPaths.erase(subPaths.begin() + static_cast<long>(i));
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderLayerManager::RestoreDeletedRenderLayer(const std::string& layerIdentifier) {
|
||||
if (!m_stage) return false;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return false;
|
||||
rootLayer->InsertSubLayerPath(layerIdentifier, 0);
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderLayerManager::RenameRenderLayer(const std::string& layerId, const std::string& newName) {
|
||||
SdfLayerHandle layer = FindRenderLayer(layerId);
|
||||
if (!layer || newName.empty()) return false;
|
||||
VtDictionary cld = layer->GetCustomLayerData();
|
||||
cld[kNameKey] = VtValue(newName);
|
||||
layer->SetCustomLayerData(cld);
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Switching
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool RenderLayerManager::ActivateRenderLayer(const std::string& layerId) {
|
||||
if (!m_stage) return false;
|
||||
SdfLayerHandle target = FindRenderLayer(layerId);
|
||||
if (!target) return false;
|
||||
|
||||
// First entry into render-layer mode this "session" (Default -> a render
|
||||
// layer): remember the edit target so ActivateDefaultLayer() can restore
|
||||
// it later, no matter how many render-layer-to-render-layer switches
|
||||
// happen in between.
|
||||
if (IsDefaultActive()) {
|
||||
SdfLayerHandle currentEt = m_stage->GetEditTarget().GetLayer();
|
||||
m_savedEditTargetIdBeforeMode = currentEt ? currentEt->GetIdentifier() : std::string();
|
||||
}
|
||||
|
||||
for (const auto& info : m_layers) {
|
||||
if (info.isDefault || info.layerIdentifier == layerId) continue;
|
||||
if (!m_stage->IsLayerMuted(info.layerIdentifier))
|
||||
m_stage->MuteLayer(info.layerIdentifier);
|
||||
}
|
||||
if (m_stage->IsLayerMuted(layerId))
|
||||
m_stage->UnmuteLayer(layerId);
|
||||
|
||||
if (m_layerManager) m_layerManager->SetEditTarget(layerId);
|
||||
else m_stage->SetEditTarget(UsdEditTarget(target));
|
||||
|
||||
RecomputeIsolation(target, layerId);
|
||||
PersistActiveLayerId(layerId);
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderLayerManager::ActivateDefaultLayer() {
|
||||
if (!m_stage) return;
|
||||
for (const auto& info : m_layers) {
|
||||
if (info.isDefault) continue;
|
||||
if (!m_stage->IsLayerMuted(info.layerIdentifier))
|
||||
m_stage->MuteLayer(info.layerIdentifier);
|
||||
}
|
||||
|
||||
if (!m_savedEditTargetIdBeforeMode.empty()) {
|
||||
if (m_layerManager) {
|
||||
m_layerManager->SetEditTarget(m_savedEditTargetIdBeforeMode);
|
||||
} else if (SdfLayerHandle saved = SdfLayer::Find(m_savedEditTargetIdBeforeMode)) {
|
||||
m_stage->SetEditTarget(UsdEditTarget(saved));
|
||||
}
|
||||
}
|
||||
m_savedEditTargetIdBeforeMode.clear();
|
||||
|
||||
PersistActiveLayerId("");
|
||||
Refresh();
|
||||
RefreshLayerManager();
|
||||
}
|
||||
|
||||
std::string RenderLayerManager::GetActiveRenderLayerId() const {
|
||||
for (const auto& info : m_layers)
|
||||
if (!info.isDefault && info.isActive)
|
||||
return info.layerIdentifier;
|
||||
return {};
|
||||
}
|
||||
|
||||
void RenderLayerManager::PersistActiveLayerId(const std::string& layerId) {
|
||||
if (!m_stage) return;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return;
|
||||
VtDictionary cld = rootLayer->GetCustomLayerData();
|
||||
if (layerId.empty()) cld.erase(kActiveLayerKey);
|
||||
else cld[kActiveLayerKey] = VtValue(layerId);
|
||||
rootLayer->SetCustomLayerData(cld);
|
||||
}
|
||||
|
||||
void RenderLayerManager::RestorePersistedActiveLayer() {
|
||||
if (!m_stage) return;
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer) return;
|
||||
VtDictionary cld = rootLayer->GetCustomLayerData();
|
||||
auto it = cld.find(kActiveLayerKey);
|
||||
if (it == cld.end() || !it->second.IsHolding<std::string>()) return;
|
||||
std::string layerId = it->second.Get<std::string>();
|
||||
if (FindRenderLayer(layerId))
|
||||
ActivateRenderLayer(layerId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Membership (Sdf-level — works regardless of mute state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<SdfPath> RenderLayerManager::GetMembers(const std::string& layerId) const {
|
||||
std::vector<SdfPath> result;
|
||||
SdfLayerHandle layer = FindRenderLayer(layerId);
|
||||
if (!layer) return result;
|
||||
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
|
||||
if (!rel) return result;
|
||||
for (const SdfPath& p : rel->GetTargetPathList().GetAddedOrExplicitItems())
|
||||
result.push_back(p);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RenderLayerManager::AddMember(const std::string& layerId, const SdfPath& path) {
|
||||
SdfLayerHandle layer = FindRenderLayer(layerId);
|
||||
if (!layer) return false;
|
||||
|
||||
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
|
||||
if (!rel) {
|
||||
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(layer, kAdminPrimPath);
|
||||
if (!adminPrim) return false;
|
||||
rel = SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
|
||||
/*custom=*/false, SdfVariabilityUniform);
|
||||
}
|
||||
if (!rel) return false;
|
||||
|
||||
// Add() doesn't de-duplicate against existing explicit items — guard
|
||||
// here so repeated adds of the same path (e.g. re-running a command, or
|
||||
// a redundant drag) don't pile up duplicate targets.
|
||||
auto existing = rel->GetTargetPathList().GetAddedOrExplicitItems();
|
||||
if (std::find(existing.begin(), existing.end(), path) == existing.end())
|
||||
rel->GetTargetPathList().Add(path);
|
||||
|
||||
if (GetActiveRenderLayerId() == layerId)
|
||||
RecomputeIsolation(layer, layerId);
|
||||
RefreshLayerManager(); // keeps StageEditorPanel's dirty-flag asterisk current
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderLayerManager::RemoveMember(const std::string& layerId, const SdfPath& path) {
|
||||
SdfLayerHandle layer = FindRenderLayer(layerId);
|
||||
if (!layer) return false;
|
||||
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
|
||||
if (!rel) return false;
|
||||
|
||||
rel->GetTargetPathList().Remove(path);
|
||||
|
||||
if (GetActiveRenderLayerId() == layerId)
|
||||
RecomputeIsolation(layer, layerId);
|
||||
RefreshLayerManager();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Viewport isolation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void RenderLayerManager::ClearAutoHiddenPaths(const SdfLayerHandle& layer) {
|
||||
if (!layer) return;
|
||||
VtDictionary cld = layer->GetCustomLayerData();
|
||||
auto it = cld.find(kAutoHiddenKey);
|
||||
if (it == cld.end()) return;
|
||||
|
||||
if (it->second.IsHolding<VtArray<std::string>>()) {
|
||||
for (const std::string& p : it->second.Get<VtArray<std::string>>()) {
|
||||
SdfPath primPath(p);
|
||||
SdfPrimSpecHandle owner = layer->GetPrimAtPath(primPath);
|
||||
SdfAttributeSpecHandle attrSpec =
|
||||
layer->GetAttributeAtPath(primPath.AppendProperty(TfToken("visibility")));
|
||||
if (owner && attrSpec)
|
||||
owner->RemoveProperty(attrSpec);
|
||||
}
|
||||
}
|
||||
cld.erase(it);
|
||||
layer->SetCustomLayerData(cld);
|
||||
}
|
||||
|
||||
void RenderLayerManager::RecomputeIsolation(const SdfLayerHandle& layer, const std::string& layerId) {
|
||||
if (!m_stage || !layer) return;
|
||||
|
||||
ClearAutoHiddenPaths(layer);
|
||||
|
||||
std::vector<SdfPath> memberList = GetMembers(layerId);
|
||||
std::set<SdfPath> members(memberList.begin(), memberList.end());
|
||||
|
||||
// Every strict prefix of every member path: a subtree rooted at one of
|
||||
// these paths might still contain a member somewhere below it, so it
|
||||
// must be recursed into rather than hidden outright.
|
||||
std::set<SdfPath> ancestors;
|
||||
for (const SdfPath& m : memberList) {
|
||||
SdfPath p = m.GetParentPath();
|
||||
while (!p.IsEmpty()) {
|
||||
if (!ancestors.insert(p).second) break; // already inserted by another member
|
||||
if (p == SdfPath::AbsoluteRootPath()) break;
|
||||
p = p.GetParentPath();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> newHidden;
|
||||
|
||||
std::function<void(const UsdPrim&)> visit = [&](const UsdPrim& prim) {
|
||||
for (const UsdPrim& child : prim.GetChildren()) {
|
||||
SdfPath cp = child.GetPath();
|
||||
if (cp == kAdminPrimPath) continue;
|
||||
|
||||
if (members.count(cp)) {
|
||||
// Exact member: whole subtree stays default-visible (expandPrims
|
||||
// semantics — everything under a member is implicitly included),
|
||||
// nothing to author, no need to recurse.
|
||||
continue;
|
||||
}
|
||||
if (ancestors.count(cp)) {
|
||||
visit(child); // a member lies somewhere below — keep exploring
|
||||
continue;
|
||||
}
|
||||
|
||||
UsdGeomImageable img(child);
|
||||
if (img) {
|
||||
UsdEditContext ec(m_stage, layer);
|
||||
img.GetVisibilityAttr().Set(UsdGeomTokens->invisible);
|
||||
newHidden.push_back(cp.GetString());
|
||||
// Minimal cut: stop here, children inherit the hidden state.
|
||||
} else {
|
||||
// Can't author visibility on a non-imageable prim (e.g. a pure
|
||||
// Scope holding unrelated data) — look deeper for an imageable
|
||||
// descendant to hide instead, rather than silently failing to
|
||||
// isolate this whole branch.
|
||||
visit(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(m_stage->GetPseudoRoot());
|
||||
|
||||
if (!newHidden.empty()) {
|
||||
VtDictionary cld = layer->GetCustomLayerData();
|
||||
cld[kAutoHiddenKey] = VtValue(VtArray<std::string>(newHidden.begin(), newHidden.end()));
|
||||
layer->SetCustomLayerData(cld);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
class LayerManager;
|
||||
|
||||
/// One row for the Render Layer panel. The "Default" entry (isDefault=true)
|
||||
/// has no backing layer — it represents the unmodified base stage, i.e. every
|
||||
/// render-layer sublayer muted.
|
||||
struct RenderLayerInfo {
|
||||
std::string name;
|
||||
pxr::SdfLayerHandle layer; ///< empty for Default
|
||||
std::string layerIdentifier; ///< empty for Default; the stable key used everywhere below
|
||||
bool isActive = false;
|
||||
bool isDefault = false;
|
||||
};
|
||||
|
||||
/// Maya-Render-Layers-style layers built on the stage's own sublayer stack.
|
||||
///
|
||||
/// Each render layer is a dedicated, file-backed SdfLayer inserted as a
|
||||
/// sublayer of the stage's root layer (tagged via SdfLayer custom layer data
|
||||
/// so it's distinguishable from ordinary content sublayers). At most one is
|
||||
/// ever unmuted ("active") at a time; activating one mutes the rest and
|
||||
/// repoints the stage's ambient edit target at it, so every existing
|
||||
/// attribute/transform/shader-connection/material-binding command in the app
|
||||
/// already lands its edits in the right place with no changes of its own
|
||||
/// (see docs/adr/0002-render-layer.md).
|
||||
///
|
||||
/// Membership (which lights/objects belong to a layer) is stored as a
|
||||
/// UsdCollectionAPI-compatible relationship (`collection:members:includes`)
|
||||
/// on a reserved, undefined ("over"-only, never rendered) admin prim inside
|
||||
/// that layer's own SdfLayer. All reads/writes of it go through raw Sdf-level
|
||||
/// API rather than UsdCollectionAPI/UsdStage, because muted layers are fully
|
||||
/// excluded from stage composition — GetPrimAtPath would fail for every
|
||||
/// layer except the currently-active one, and the panel needs to edit
|
||||
/// membership on layers that aren't active.
|
||||
///
|
||||
/// Switching layers also isolates the viewport to that layer's members: a
|
||||
/// single top-down traversal authors the minimal set of
|
||||
/// `visibility = invisible` opinions (into the active layer's own SdfLayer)
|
||||
/// needed to hide every subtree that contains no member, tracked via that
|
||||
/// layer's custom layer data so it can be precisely cleared and recomputed
|
||||
/// without disturbing deliberate visibility overrides.
|
||||
class RenderLayerManager {
|
||||
public:
|
||||
RenderLayerManager();
|
||||
~RenderLayerManager();
|
||||
|
||||
void SetStage(pxr::UsdStageRefPtr stage);
|
||||
void SetLayerManager(LayerManager* mgr) { m_layerManager = mgr; }
|
||||
void Refresh();
|
||||
|
||||
/// Default first, then user-created render layers in sublayer order.
|
||||
std::vector<RenderLayerInfo> GetRenderLayers() const { return m_layers; }
|
||||
|
||||
/// Creates a new file-backed render layer next to the stage's root layer
|
||||
/// (under a ".renderlayers" subdirectory), muted, with an empty member
|
||||
/// list. Returns the new layer's identifier, or empty on failure.
|
||||
std::string CreateRenderLayer(const std::string& name);
|
||||
bool DeleteRenderLayer(const std::string& layerId);
|
||||
/// Re-inserts a render layer previously removed by DeleteRenderLayer — its
|
||||
/// backing file is left on disk untouched by DeleteRenderLayer, so this
|
||||
/// just restores the sublayer reference (always at the top of the stack,
|
||||
/// same as a newly-created layer). Used by RenderLayerDeleteCommand::Undo().
|
||||
bool RestoreDeletedRenderLayer(const std::string& layerIdentifier);
|
||||
bool RenameRenderLayer(const std::string& layerId, const std::string& newName);
|
||||
|
||||
/// Mutes every other render-layer sublayer, unmutes layerId, repoints the
|
||||
/// stage's ambient edit target at it, and recomputes viewport isolation.
|
||||
/// The first time this is called while Default is active, it remembers
|
||||
/// the current edit target so ActivateDefaultLayer() can restore it later
|
||||
/// — callers (e.g. an undoable switch command) don't need to manage that
|
||||
/// themselves; it's tracked here so it stays correct across Undo/Redo.
|
||||
bool ActivateRenderLayer(const std::string& layerId);
|
||||
/// Mutes every render-layer sublayer (back to the unmodified base stage)
|
||||
/// and restores whatever the edit target was before render-layer mode
|
||||
/// was first entered (see ActivateRenderLayer's note).
|
||||
void ActivateDefaultLayer();
|
||||
std::string GetActiveRenderLayerId() const;
|
||||
bool IsDefaultActive() const { return GetActiveRenderLayerId().empty(); }
|
||||
|
||||
/// Sdf-level: the raw, unexpanded `includes` target list. Works
|
||||
/// regardless of whether layerId is currently muted.
|
||||
std::vector<pxr::SdfPath> GetMembers(const std::string& layerId) const;
|
||||
bool AddMember(const std::string& layerId, const pxr::SdfPath& path);
|
||||
bool RemoveMember(const std::string& layerId, const pxr::SdfPath& path);
|
||||
|
||||
/// Re-applies whichever render layer was active the last time this stage
|
||||
/// was saved (persisted as root-layer custom layer data, since USD mute
|
||||
/// state itself is a runtime flag and isn't serialized). Call once after
|
||||
/// SetStage(). No-op if nothing was persisted, or the persisted layer no
|
||||
/// longer exists.
|
||||
void RestorePersistedActiveLayer();
|
||||
|
||||
/// True if this layer carries the "this sublayer is a render layer"
|
||||
/// marker RenderLayerManager itself authors on creation. Exposed so
|
||||
/// other panels (e.g. StageEditorPanel) that walk the raw sublayer list
|
||||
/// can recognize and guard-rail render-layer sublayers without
|
||||
/// duplicating the custom-layer-data key.
|
||||
static bool IsRenderLayerSublayer(const pxr::SdfLayerHandle& layer);
|
||||
|
||||
private:
|
||||
void BuildList();
|
||||
// LayerManager keeps its own cached sublayer/mute-state snapshot,
|
||||
// rebuilt only on its own SetStage()/Refresh()/mutator calls — it has no
|
||||
// way to know when RenderLayerManager mutates the root layer's sublayer
|
||||
// stack or a layer's mute state directly (bypassing LayerManager's own
|
||||
// methods, which every RenderLayerManager mutator below does). Called
|
||||
// after every such mutation so StageEditorPanel (backed by LayerManager)
|
||||
// doesn't show a stale sublayer list.
|
||||
void RefreshLayerManager();
|
||||
pxr::SdfLayerHandle FindRenderLayer(const std::string& layerId) const;
|
||||
void RecomputeIsolation(const pxr::SdfLayerHandle& layer, const std::string& layerId);
|
||||
void ClearAutoHiddenPaths(const pxr::SdfLayerHandle& layer);
|
||||
void PersistActiveLayerId(const std::string& layerId);
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
LayerManager* m_layerManager = nullptr;
|
||||
std::vector<RenderLayerInfo> m_layers; // [0] is always the Default entry
|
||||
// Strong refs so muted render-layer SdfLayers stay alive between
|
||||
// Refresh() calls (mirrors LayerManager::m_layerRefs).
|
||||
std::vector<pxr::SdfLayerRefPtr> m_layerRefs;
|
||||
// Edit target identifier captured the moment render-layer mode is first
|
||||
// entered (Default -> any render layer); restored and cleared by
|
||||
// ActivateDefaultLayer(). Empty when not currently "in mode".
|
||||
std::string m_savedEditTargetIdBeforeMode;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -8,6 +8,14 @@
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/usd/usdGeom/bboxCache.h>
|
||||
#include <pxr/usd/usdGeom/camera.h>
|
||||
#include <pxr/usd/usdGeom/xformCache.h>
|
||||
#include <pxr/usd/usdLux/lightAPI.h>
|
||||
#include <pxr/usd/usdLux/sphereLight.h>
|
||||
#include <pxr/usd/usdLux/rectLight.h>
|
||||
#include <pxr/usd/usdLux/diskLight.h>
|
||||
#include <pxr/usd/usdLux/distantLight.h>
|
||||
#include <pxr/usd/usdLux/domeLight.h>
|
||||
#include <pxr/usd/usdLux/cylinderLight.h>
|
||||
#include <pxr/imaging/glf/contextCaps.h>
|
||||
#include <pxr/imaging/cameraUtil/conformWindow.h>
|
||||
#include <pxr/imaging/cameraUtil/framing.h>
|
||||
@@ -15,8 +23,15 @@
|
||||
#include <pxr/base/gf/rect2i.h>
|
||||
#include <pxr/base/gf/frustum.h>
|
||||
#include <pxr/base/gf/rotation.h>
|
||||
#include <pxr/base/tf/diagnosticMgr.h>
|
||||
#include <pxr/base/tf/error.h>
|
||||
#include <pxr/base/tf/warning.h>
|
||||
#include <pxr/base/tf/status.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
@@ -30,6 +45,37 @@ static std::string Vec3fStr(const pxr::GfVec3f& v) {
|
||||
+ std::to_string(v[2]) + ")";
|
||||
}
|
||||
|
||||
// Forwards USD's TfError / TF_WARN / TF_STATUS messages into the app log.
|
||||
// Without this, Hydra diagnostics (e.g. the reason HdxColorCorrectionTask
|
||||
// fails to build an OCIO processor) only go to a stderr console the GUI
|
||||
// never shows. Installed once, process-wide, from InitRenderer().
|
||||
namespace {
|
||||
class UsdDiagnosticLogger : public pxr::TfDiagnosticMgr::Delegate {
|
||||
public:
|
||||
void IssueError(pxr::TfError const& err) override {
|
||||
LOG_ERROR("[USD] " + err.GetCommentary());
|
||||
}
|
||||
void IssueFatalError(pxr::TfCallContext const&,
|
||||
std::string const& msg) override {
|
||||
LOG_ERROR("[USD fatal] " + msg);
|
||||
}
|
||||
void IssueStatus(pxr::TfStatus const& status) override {
|
||||
LOG_INFO("[USD] " + status.GetCommentary());
|
||||
}
|
||||
void IssueWarning(pxr::TfWarning const& warning) override {
|
||||
LOG_WARNING("[USD] " + warning.GetCommentary());
|
||||
}
|
||||
};
|
||||
|
||||
void InstallUsdDiagnosticLogger() {
|
||||
static UsdDiagnosticLogger s_logger; // process-lifetime; never removed
|
||||
static bool s_installed = false;
|
||||
if (s_installed) return;
|
||||
s_installed = true;
|
||||
pxr::TfDiagnosticMgr::GetInstance().AddDelegate(&s_logger);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/// Port of stageView._ComputeCameraFraming():
|
||||
/// Converts a Y-up integer viewport rect into a CameraUtilFraming whose
|
||||
/// display/data windows are expressed in the Y-down coordinate system that
|
||||
@@ -120,8 +166,16 @@ void main() { outColor = color; }
|
||||
|
||||
UsdSceneRenderer::UsdSceneRenderer()
|
||||
: m_showGrid(true)
|
||||
, m_showCameraGuide(false)
|
||||
, m_showGuides(false)
|
||||
, m_showProxy(true)
|
||||
, m_showRender(false)
|
||||
, m_currentAov(pxr::TfToken("color"))
|
||||
, m_aaEnabled(false)
|
||||
, m_backgroundColor(0.15f, 0.15f, 0.15f)
|
||||
, m_shadingMode(ShadingMode::SmoothShaded)
|
||||
, m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
|
||||
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
|
||||
, m_ambientLightOnly(true)
|
||||
, m_domeLightEnabled(false)
|
||||
, m_stageIsZup(false)
|
||||
@@ -160,6 +214,7 @@ UsdSceneRenderer::~UsdSceneRenderer() {
|
||||
DestroyAxisResources();
|
||||
DestroyBBoxResources();
|
||||
DestroyCamWireResources();
|
||||
DestroyLightWireResources();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -177,6 +232,7 @@ void UsdSceneRenderer::SetStage(pxr::UsdStageRefPtr stage) {
|
||||
DestroyAxisResources();
|
||||
DestroyBBoxResources();
|
||||
DestroyCamWireResources();
|
||||
DestroyLightWireResources();
|
||||
|
||||
// Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup)
|
||||
if (stage) {
|
||||
@@ -199,6 +255,9 @@ void UsdSceneRenderer::InitRenderer() {
|
||||
|
||||
LOG_INFO("UsdSceneRenderer::InitRenderer - initializing...");
|
||||
|
||||
// Route USD/Hydra diagnostics (incl. OCIO failures) to the app log.
|
||||
InstallUsdDiagnosticLogger();
|
||||
|
||||
pxr::GlfContextCaps::InitInstance();
|
||||
|
||||
pxr::UsdImagingGLEngine::Parameters params;
|
||||
@@ -213,31 +272,37 @@ void UsdSceneRenderer::InitRenderer() {
|
||||
// Plugin selection: prefer HdStorm / GL-based renderers
|
||||
auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins();
|
||||
LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size()));
|
||||
bool hasCycles = false;
|
||||
for (const auto& p : plugins) {
|
||||
LOG_INFO(" " + std::string(p.GetText()) + " -> "
|
||||
+ pxr::UsdImagingGLEngine::GetRendererDisplayName(p));
|
||||
if (std::string(p.GetText()) == "HdCyclesPlugin") hasCycles = true;
|
||||
}
|
||||
if (!hasCycles)
|
||||
LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors).");
|
||||
|
||||
pxr::TfToken currentPlugin = m_renderer->GetCurrentRendererId();
|
||||
if (currentPlugin.IsEmpty() && !plugins.empty()) {
|
||||
// Apply the requested plugin. m_currentRendererPlugin is set from global
|
||||
// preferences before InitRenderer runs; always honour it rather than
|
||||
// checking whether the engine already chose something by default.
|
||||
if (!m_currentRendererPlugin.IsEmpty()) {
|
||||
pxr::TfToken cur = m_renderer->GetCurrentRendererId();
|
||||
if (cur != m_currentRendererPlugin)
|
||||
m_renderer->SetRendererPlugin(m_currentRendererPlugin);
|
||||
} else if (!plugins.empty()) {
|
||||
// Fallback heuristic: prefer Storm/GL
|
||||
pxr::TfToken best;
|
||||
// If a plugin was previously selected, honour it
|
||||
if (!m_currentRendererPlugin.IsEmpty()) {
|
||||
best = m_currentRendererPlugin;
|
||||
} else {
|
||||
for (const auto& p : plugins) {
|
||||
std::string n(p.GetText());
|
||||
if (n.find("Storm") != std::string::npos ||
|
||||
n.find("GL") != std::string::npos) { best = p; break; }
|
||||
}
|
||||
if (best.IsEmpty()) best = plugins[0];
|
||||
for (const auto& p : plugins) {
|
||||
std::string n(p.GetText());
|
||||
if (n.find("Storm") != std::string::npos ||
|
||||
n.find("GL") != std::string::npos) { best = p; break; }
|
||||
}
|
||||
if (best.IsEmpty()) best = plugins[0];
|
||||
m_renderer->SetRendererPlugin(best);
|
||||
LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));
|
||||
}
|
||||
LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));
|
||||
|
||||
// Enable AOV "color" (matches stageView._handleRendererChanged)
|
||||
m_renderer->SetRendererAov(pxr::TfToken("color"));
|
||||
// Enable AOV (matches stageView._handleRendererChanged; uses saved m_currentAov)
|
||||
m_renderer->SetRendererAov(m_currentAov);
|
||||
|
||||
// Selection highlight color (usdview default: yellow)
|
||||
m_renderer->SetSelectionColor(pxr::GfVec4f(1.0f, 1.0f, 0.0f, 1.0f));
|
||||
@@ -252,6 +317,7 @@ void UsdSceneRenderer::InitRenderer() {
|
||||
InitAxisResources();
|
||||
InitBBoxResources();
|
||||
InitCamWireResources();
|
||||
InitLightWireResources();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -281,8 +347,9 @@ bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
|
||||
|
||||
bool ok = m_renderer->SetRendererPlugin(pluginId);
|
||||
if (ok) {
|
||||
// Re-enable colour AOV after delegate switch (mirrors usdtweak behaviour)
|
||||
m_renderer->SetRendererAov(pxr::TfToken("color"));
|
||||
// Reset AOV to "color" on every delegate switch (mirrors stageView._handleRendererChanged)
|
||||
m_currentAov = pxr::TfToken("color");
|
||||
m_renderer->SetRendererAov(m_currentAov);
|
||||
LOG_INFO("Render delegate switched to: " + std::string(pluginId.GetText()));
|
||||
} else {
|
||||
LOG_ERROR("Failed to switch render delegate to: " + std::string(pluginId.GetText()));
|
||||
@@ -290,6 +357,39 @@ bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
pxr::TfTokenVector UsdSceneRenderer::GetRendererAovs() const
|
||||
{
|
||||
if (!m_renderer) return {};
|
||||
return m_renderer->GetRendererAovs();
|
||||
}
|
||||
|
||||
bool UsdSceneRenderer::SetCurrentAov(const pxr::TfToken& aov)
|
||||
{
|
||||
if (!m_renderer) return false;
|
||||
if (m_renderer->SetRendererAov(aov)) {
|
||||
m_currentAov = aov;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pxr::UsdImagingGLRendererSettingsList UsdSceneRenderer::GetRendererSettingsList() const
|
||||
{
|
||||
if (!m_renderer) return {};
|
||||
return m_renderer->GetRendererSettingsList();
|
||||
}
|
||||
|
||||
pxr::VtValue UsdSceneRenderer::GetRendererSetting(const pxr::TfToken& id) const
|
||||
{
|
||||
if (!m_renderer) return {};
|
||||
return m_renderer->GetRendererSetting(id);
|
||||
}
|
||||
|
||||
void UsdSceneRenderer::SetRendererSetting(const pxr::TfToken& id, const pxr::VtValue& v)
|
||||
{
|
||||
if (m_renderer) m_renderer->SetRendererSetting(id, v);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Camera State
|
||||
// ===========================================================================
|
||||
@@ -356,7 +456,7 @@ pxr::GfRange3d UsdSceneRenderer::ComputeStageBounds()
|
||||
if (!m_stage) return {};
|
||||
pxr::TfTokenVector purposes = {
|
||||
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
|
||||
pxr::UsdGeomBBoxCache bboxCache(pxr::UsdTimeCode::Default(), purposes, true);
|
||||
pxr::UsdGeomBBoxCache bboxCache(m_currentTime, purposes, true);
|
||||
return bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()).ComputeAlignedRange();
|
||||
}
|
||||
|
||||
@@ -485,6 +585,8 @@ bool UsdSceneRenderer::PickObjectsInRect(
|
||||
void UsdSceneRenderer::Render(int width, int height)
|
||||
{
|
||||
if (!m_stage || width <= 0 || height <= 0) return;
|
||||
m_lastRenderWidth = width;
|
||||
m_lastRenderHeight = height;
|
||||
|
||||
InitRenderer();
|
||||
if (!m_renderer) return;
|
||||
@@ -503,7 +605,9 @@ void UsdSceneRenderer::Render(int width, int height)
|
||||
m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA);
|
||||
if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; }
|
||||
m_drawTarget->Bind();
|
||||
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA);
|
||||
// RGBA16F: Hydra's internal render buffers carry linear HDR values;
|
||||
// RGBA16F on the draw target preserves precision in the corrected output.
|
||||
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F);
|
||||
m_drawTarget->AddAttachment("depth",
|
||||
GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F);
|
||||
m_drawTarget->Unbind();
|
||||
@@ -560,7 +664,13 @@ void UsdSceneRenderer::Render(int width, int height)
|
||||
// Camera headlight: point light (w=1) positioned at the camera world-origin,
|
||||
// transformed by the view-inverse so it tracks the camera each frame.
|
||||
// (stageView.py: l.position = cam_pos + (1,); l.transform = frustum.ComputeViewInverse())
|
||||
if (m_ambientLightOnly) {
|
||||
//
|
||||
// The headlight is the *default* fill used only while the stage has no
|
||||
// authored lights. Once the scene contains real UsdLux lights, suppress it
|
||||
// so the scene is lit purely by those lights (Hydra evaluates them via
|
||||
// enableSceneLights) -- mirrors usdview's "use scene lights when present".
|
||||
const bool stageHasLights = StageHasAuthoredLights();
|
||||
if (m_ambientLightOnly && !stageHasLights) {
|
||||
pxr::GfMatrix4d viewInverse = m_viewMatrix.GetInverse();
|
||||
pxr::GfVec3d camPos = viewInverse.ExtractTranslation();
|
||||
|
||||
@@ -594,16 +704,32 @@ void UsdSceneRenderer::Render(int width, int height)
|
||||
|
||||
// --- Render params (matches stageView.renderSinglePass) ---
|
||||
m_renderParams = pxr::UsdImagingGLRenderParams();
|
||||
m_renderParams.frame = pxr::UsdTimeCode::Default();
|
||||
m_renderParams.complexity = 1.0f;
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
|
||||
m_renderParams.showGuides = true;
|
||||
m_renderParams.showProxy = true;
|
||||
m_renderParams.showRender = false;
|
||||
m_renderParams.enableLighting = true;
|
||||
m_renderParams.frame = m_currentTime;
|
||||
m_renderParams.complexity = m_complexity;
|
||||
// Apply shading mode → drawMode + enableLighting
|
||||
switch (m_shadingMode) {
|
||||
case ShadingMode::FlatShaded:
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_FLAT;
|
||||
m_renderParams.enableLighting = true; break;
|
||||
case ShadingMode::WireframeOnSurface:
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME_ON_SURFACE;
|
||||
m_renderParams.enableLighting = true; break;
|
||||
case ShadingMode::Wireframe:
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME;
|
||||
m_renderParams.enableLighting = false; break;
|
||||
case ShadingMode::Unlit:
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
|
||||
m_renderParams.enableLighting = false; break;
|
||||
default: // SmoothShaded
|
||||
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
|
||||
m_renderParams.enableLighting = true; break;
|
||||
}
|
||||
m_renderParams.showGuides = m_showGuides;
|
||||
m_renderParams.showProxy = m_showProxy;
|
||||
m_renderParams.showRender = m_showRender;
|
||||
m_renderParams.enableSampleAlphaToCoverage = true;
|
||||
m_renderParams.gammaCorrectColors = false;
|
||||
m_renderParams.cullStyle = pxr::UsdImagingGLCullStyle::CULL_STYLE_BACK_UNLESS_DOUBLE_SIDED;
|
||||
m_renderParams.cullStyle = static_cast<pxr::UsdImagingGLCullStyle>(m_cullStyle);
|
||||
m_renderParams.enableSceneMaterials = true;
|
||||
m_renderParams.enableSceneLights = true;
|
||||
m_renderParams.highlight = true;
|
||||
@@ -612,7 +738,37 @@ void UsdSceneRenderer::Render(int width, int height)
|
||||
m_renderParams.forceRefresh = m_forceRefresh;
|
||||
m_renderParams.clipPlanes = m_clipPlanes;
|
||||
|
||||
m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams);
|
||||
// Color correction — stageView.py approach: pass the actual mode + OCIO
|
||||
// params to HdxColorCorrectionTask via RenderParams and SetColorCorrection-
|
||||
// Settings (mirrors stageView.renderSinglePass).
|
||||
pxr::TfToken ccToken("disabled");
|
||||
if (m_colorCorrectionMode == ColorCorrectionMode::sRGB)
|
||||
ccToken = pxr::TfToken("sRGB");
|
||||
else if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO)
|
||||
ccToken = pxr::TfToken("openColorIO");
|
||||
|
||||
m_renderParams.colorCorrectionMode = ccToken;
|
||||
if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO) {
|
||||
m_renderParams.ocioDisplay = pxr::TfToken(m_ocioDisplay);
|
||||
m_renderParams.ocioView = pxr::TfToken(m_ocioView);
|
||||
m_renderParams.ocioColorSpace = pxr::TfToken(m_ocioColorSpace);
|
||||
m_renderParams.ocioLook = pxr::TfToken(m_ocioLook);
|
||||
}
|
||||
m_renderer->SetColorCorrectionSettings(
|
||||
ccToken,
|
||||
pxr::TfToken(m_ocioDisplay),
|
||||
pxr::TfToken(m_ocioView),
|
||||
pxr::TfToken(m_ocioColorSpace),
|
||||
pxr::TfToken(m_ocioLook));
|
||||
|
||||
// Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below
|
||||
// would be skipped, permanently unbalancing the GlfDrawTarget bind stack
|
||||
// and blacking out every later frame.
|
||||
try {
|
||||
m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Hydra render failed: " + std::string(e.what()));
|
||||
}
|
||||
m_forceRefresh = false;
|
||||
|
||||
// --- Optional grid overlay ---
|
||||
@@ -653,12 +809,45 @@ uint32_t UsdSceneRenderer::GetColorTextureID()
|
||||
// Resolve MSAA → regular texture before the caller samples it.
|
||||
// Called after all overlay draws (axis, bboxes, camera wireframes) so
|
||||
// every layer of MSAA-rendered content is included in the resolve.
|
||||
// No-op when MSAA is not enabled.
|
||||
m_drawTarget->Resolve();
|
||||
auto att = m_drawTarget->GetAttachment("color");
|
||||
return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0;
|
||||
}
|
||||
|
||||
bool UsdSceneRenderer::CaptureFrame(std::vector<uint8_t>& outRGBA)
|
||||
{
|
||||
if (!m_drawTarget || m_lastRenderWidth <= 0 || m_lastRenderHeight <= 0)
|
||||
return false;
|
||||
|
||||
int w = m_lastRenderWidth;
|
||||
int h = m_lastRenderHeight;
|
||||
|
||||
// Resolve MSAA so we read from the non-multisampled colour attachment.
|
||||
m_drawTarget->Resolve();
|
||||
|
||||
outRGBA.resize(static_cast<size_t>(w) * h * 4);
|
||||
|
||||
// Save/restore the read-framebuffer binding so we don't upset ImGui.
|
||||
GLint prevFbo = 0;
|
||||
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevFbo);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
|
||||
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, outRGBA.data());
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevFbo));
|
||||
|
||||
// OpenGL reads bottom-up; flip to top-down.
|
||||
int stride = w * 4;
|
||||
std::vector<uint8_t> row(stride);
|
||||
for (int y = 0; y < h / 2; ++y) {
|
||||
uint8_t* top = outRGBA.data() + y * stride;
|
||||
uint8_t* bot = outRGBA.data() + (h - 1 - y) * stride;
|
||||
std::copy(top, top + stride, row.data());
|
||||
std::copy(bot, bot + stride, top);
|
||||
std::copy(row.data(), row.data() + stride, bot);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Axis Overlay (stageView.DrawAxis port)
|
||||
// ===========================================================================
|
||||
@@ -1046,6 +1235,30 @@ void UsdSceneRenderer::DestroyCamWireResources()
|
||||
if (m_camWireVBO) { glDeleteBuffers(1, &m_camWireVBO); m_camWireVBO = 0; }
|
||||
}
|
||||
|
||||
void UsdSceneRenderer::InitLightWireResources()
|
||||
{
|
||||
if (m_lightWireVAO != 0) return;
|
||||
if (!m_bboxProgram) return; // reuse the already-compiled bbox shader program
|
||||
|
||||
glGenVertexArrays(1, &m_lightWireVAO);
|
||||
glGenBuffers(1, &m_lightWireVBO);
|
||||
|
||||
glBindVertexArray(m_lightWireVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
|
||||
// Sphere/cylinder gizmos use several circles; budget more than the camera VBO.
|
||||
glBufferData(GL_ARRAY_BUFFER, 1024 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glBindVertexArray(0);
|
||||
LOG_INFO("Light wireframe GL resources initialized.");
|
||||
}
|
||||
|
||||
void UsdSceneRenderer::DestroyLightWireResources()
|
||||
{
|
||||
if (m_lightWireVAO) { glDeleteVertexArrays(1, &m_lightWireVAO); m_lightWireVAO = 0; }
|
||||
if (m_lightWireVBO) { glDeleteBuffers(1, &m_lightWireVBO); m_lightWireVBO = 0; }
|
||||
}
|
||||
|
||||
void UsdSceneRenderer::DrawBox(
|
||||
const pxr::GfRange3d& range,
|
||||
const pxr::GfMatrix4f& mvp)
|
||||
@@ -1101,7 +1314,7 @@ void UsdSceneRenderer::DrawBoundingBoxes(
|
||||
pxr::TfTokenVector purposes = {
|
||||
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
|
||||
pxr::UsdGeomBBoxCache bboxCache(
|
||||
pxr::UsdTimeCode::Default(), purposes, /*useExtentsHint=*/true);
|
||||
m_currentTime, purposes, /*useExtentsHint=*/true);
|
||||
|
||||
// Save/restore GL state
|
||||
GLboolean prevDepthMask;
|
||||
@@ -1291,6 +1504,26 @@ void UsdSceneRenderer::BuildCameraWireframeLines(const pxr::GfCamera& gfCam,
|
||||
push(arrowBase, arrowTip);
|
||||
}
|
||||
|
||||
// Builds near quad + far quad + 4 connecting edges for the full GfFrustum.
|
||||
// Mirrors stageView.py DrawCameraGuides: corners via GfFrustum::ComputeCorners(),
|
||||
// line pairs via the same 24-index layout. Appends 24 XYZ float triples.
|
||||
static void BuildCameraGuideLines(const pxr::GfCamera& gfCam,
|
||||
std::vector<float>& outVerts)
|
||||
{
|
||||
const std::vector<pxr::GfVec3d> c = gfCam.GetFrustum().ComputeCorners();
|
||||
// 0=LBN 1=RBN 2=LTN 3=RTN 4=LBF 5=RBF 6=LTF 7=RTF
|
||||
static const int kIdx[24] = {
|
||||
0,1, 1,3, 3,2, 2,0, // near quad
|
||||
4,5, 5,7, 7,6, 6,4, // far quad
|
||||
3,7, 0,4, 1,5, 2,6 // connecting edges
|
||||
};
|
||||
for (int i : kIdx) {
|
||||
outVerts.push_back(static_cast<float>(c[i][0]));
|
||||
outVerts.push_back(static_cast<float>(c[i][1]));
|
||||
outVerts.push_back(static_cast<float>(c[i][2]));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// DrawCameraWireframes
|
||||
// ===========================================================================
|
||||
@@ -1345,12 +1578,14 @@ void UsdSceneRenderer::DrawCameraWireframes(
|
||||
|
||||
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
|
||||
|
||||
std::vector<float> guideVerts; // accumulates frustum oracle lines
|
||||
|
||||
for (const auto& camPath : m_cachedCameraPaths) {
|
||||
pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
|
||||
if (!prim) continue;
|
||||
|
||||
pxr::UsdGeomCamera usdCam(prim);
|
||||
pxr::GfCamera gfCam = usdCam.GetCamera(pxr::UsdTimeCode::Default());
|
||||
pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);
|
||||
|
||||
// Determine colour
|
||||
bool isSelected = (selectedSet.count(camPath.GetString()) > 0);
|
||||
@@ -1380,6 +1615,23 @@ void UsdSceneRenderer::DrawCameraWireframes(
|
||||
glBindVertexArray(m_camWireVAO);
|
||||
glDrawArrays(GL_LINES, 0, vertCount);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Accumulate full-frustum oracle lines for non-active cameras (stageView.DrawCameraGuides)
|
||||
if (m_showCameraGuide && !isActive)
|
||||
BuildCameraGuideLines(gfCam, guideVerts);
|
||||
}
|
||||
|
||||
// Draw all camera guide frustum boxes in one call (oracle orange from stageView.py)
|
||||
if (m_showCameraGuide && !guideVerts.empty()) {
|
||||
glUniform4f(m_bboxUniformColor, 0.82745f, 0.39608f, 0.16471f, 1.0f);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>(guideVerts.size() * sizeof(float)),
|
||||
guideVerts.data(), GL_DYNAMIC_DRAW);
|
||||
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
|
||||
glBindVertexArray(m_camWireVAO);
|
||||
glDrawArrays(GL_LINES, 0, static_cast<int>(guideVerts.size() / 3));
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// Restore GL state
|
||||
@@ -1392,6 +1644,205 @@ void UsdSceneRenderer::DrawCameraWireframes(
|
||||
m_drawTarget->Unbind();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// BuildLightWireframeLines
|
||||
//
|
||||
// Geometry is built in the light's LOCAL space (using its schema attributes in
|
||||
// local units) and every vertex is pushed through localToWorld, so position,
|
||||
// orientation and scale are all handled uniformly. USD lights emit along local
|
||||
// -Z (rect/disk/distant); cylinder length runs along local X.
|
||||
// ===========================================================================
|
||||
|
||||
void UsdSceneRenderer::BuildLightWireframeLines(const pxr::UsdPrim& lightPrim,
|
||||
const pxr::GfMatrix4d& l2w,
|
||||
double scale,
|
||||
std::vector<float>& outVerts)
|
||||
{
|
||||
using pxr::GfVec3d;
|
||||
static constexpr int kSeg = 24;
|
||||
static constexpr double kTwoPi = 6.283185307179586;
|
||||
|
||||
auto pushLine = [&](const GfVec3d& a, const GfVec3d& b) {
|
||||
GfVec3d wa = l2w.Transform(a);
|
||||
GfVec3d wb = l2w.Transform(b);
|
||||
outVerts.push_back((float)wa[0]); outVerts.push_back((float)wa[1]); outVerts.push_back((float)wa[2]);
|
||||
outVerts.push_back((float)wb[0]); outVerts.push_back((float)wb[1]); outVerts.push_back((float)wb[2]);
|
||||
};
|
||||
// Circle centred at c, spanned by unit axes u,v, radius r (local space).
|
||||
auto circle = [&](const GfVec3d& c, const GfVec3d& u, const GfVec3d& v, double r) {
|
||||
GfVec3d prev;
|
||||
for (int i = 0; i <= kSeg; ++i) {
|
||||
double a = kTwoPi * double(i) / double(kSeg);
|
||||
GfVec3d p = c + u * (r * std::cos(a)) + v * (r * std::sin(a));
|
||||
if (i > 0) pushLine(prev, p);
|
||||
prev = p;
|
||||
}
|
||||
};
|
||||
|
||||
const GfVec3d X(1,0,0), Y(0,1,0), Z(0,0,1), O(0,0,0);
|
||||
const double marker = scale * 0.5; // display-scaled size for direction lines / markers
|
||||
|
||||
if (lightPrim.IsA<pxr::UsdLuxRectLight>()) {
|
||||
float w = 1.0f, h = 1.0f;
|
||||
pxr::UsdLuxRectLight rl(lightPrim);
|
||||
rl.GetWidthAttr().Get(&w, m_currentTime);
|
||||
rl.GetHeightAttr().Get(&h, m_currentTime);
|
||||
double hw = w * 0.5, hh = h * 0.5;
|
||||
GfVec3d bl(-hw,-hh,0), br(hw,-hh,0), tr(hw,hh,0), tl(-hw,hh,0);
|
||||
pushLine(bl,br); pushLine(br,tr); pushLine(tr,tl); pushLine(tl,bl);
|
||||
pushLine(O, GfVec3d(0,0,-marker)); // emission direction (-Z)
|
||||
}
|
||||
else if (lightPrim.IsA<pxr::UsdLuxDiskLight>()) {
|
||||
float r = 0.5f; pxr::UsdLuxDiskLight(lightPrim).GetRadiusAttr().Get(&r, m_currentTime);
|
||||
circle(O, X, Y, r);
|
||||
pushLine(O, GfVec3d(0,0,-marker));
|
||||
}
|
||||
else if (lightPrim.IsA<pxr::UsdLuxCylinderLight>()) {
|
||||
float r = 0.5f, len = 1.0f;
|
||||
pxr::UsdLuxCylinderLight cl(lightPrim);
|
||||
cl.GetRadiusAttr().Get(&r, m_currentTime);
|
||||
cl.GetLengthAttr().Get(&len, m_currentTime);
|
||||
double hx = len * 0.5;
|
||||
circle(GfVec3d( hx,0,0), Y, Z, r); // end caps (in local YZ)
|
||||
circle(GfVec3d(-hx,0,0), Y, Z, r);
|
||||
pushLine(GfVec3d(-hx, r,0), GfVec3d(hx, r,0)); // connecting lines
|
||||
pushLine(GfVec3d(-hx,-r,0), GfVec3d(hx,-r,0));
|
||||
pushLine(GfVec3d(-hx,0, r), GfVec3d(hx,0, r));
|
||||
pushLine(GfVec3d(-hx,0,-r), GfVec3d(hx,0,-r));
|
||||
}
|
||||
else if (lightPrim.IsA<pxr::UsdLuxDistantLight>()) {
|
||||
// Sun: small disc facing -Z plus parallel rays along -Z.
|
||||
double r = marker;
|
||||
circle(O, X, Y, r);
|
||||
const GfVec3d off[5] = { O, GfVec3d(r,0,0), GfVec3d(-r,0,0), GfVec3d(0,r,0), GfVec3d(0,-r,0) };
|
||||
for (const auto& o : off) pushLine(o, o + GfVec3d(0,0,-marker*2.0));
|
||||
}
|
||||
else if (lightPrim.IsA<pxr::UsdLuxDomeLight>()) {
|
||||
// Environment dome: 3 large display-scaled circles around the origin.
|
||||
double r = std::max(scale, 1.0);
|
||||
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
|
||||
}
|
||||
else if (lightPrim.IsA<pxr::UsdLuxSphereLight>()) {
|
||||
float r = 0.5f; pxr::UsdLuxSphereLight sl(lightPrim);
|
||||
sl.GetRadiusAttr().Get(&r, m_currentTime);
|
||||
bool pointish = false; sl.GetTreatAsPointAttr().Get(&pointish, m_currentTime);
|
||||
if (pointish || r < 1e-4f) {
|
||||
double s = marker; // point light: axis cross + tiny disc
|
||||
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
|
||||
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
|
||||
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
|
||||
circle(O, X, Y, s * 0.4);
|
||||
} else {
|
||||
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
|
||||
}
|
||||
}
|
||||
else {
|
||||
double s = marker; // unknown light: generic point marker
|
||||
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
|
||||
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
|
||||
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// StageHasAuthoredLights
|
||||
// ===========================================================================
|
||||
|
||||
bool UsdSceneRenderer::StageHasAuthoredLights() const
|
||||
{
|
||||
if (!m_stage) return false;
|
||||
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
|
||||
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// DrawLightWireframes (mirrors DrawCameraWireframes)
|
||||
// ===========================================================================
|
||||
|
||||
void UsdSceneRenderer::DrawLightWireframes(
|
||||
pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPathVector& selectedPaths,
|
||||
const pxr::GfMatrix4d& viewProjMatrix,
|
||||
double viewportCameraDist)
|
||||
{
|
||||
if (!stage) return;
|
||||
if (!m_bboxProgram || !m_lightWireVAO || !m_lightWireVBO) return;
|
||||
if (!m_drawTarget) return;
|
||||
|
||||
// Collect every light prim (anything carrying UsdLuxLightAPI).
|
||||
pxr::SdfPathVector lightPaths;
|
||||
for (const pxr::UsdPrim& prim : stage->Traverse()) {
|
||||
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
|
||||
lightPaths.push_back(prim.GetPath());
|
||||
}
|
||||
if (lightPaths.empty()) return;
|
||||
|
||||
double rawScale = viewportCameraDist * 0.12;
|
||||
double scale = std::min(rawScale, 50.0);
|
||||
scale = std::max(scale, 0.5);
|
||||
|
||||
std::unordered_set<std::string> selectedSet;
|
||||
for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());
|
||||
|
||||
pxr::GfVec2i sz = m_drawTarget->GetSize();
|
||||
m_drawTarget->Bind();
|
||||
glViewport(0, 0, sz[0], sz[1]);
|
||||
|
||||
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prevDepthMask; glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
|
||||
GLboolean prevBlend = glIsEnabled(GL_BLEND);
|
||||
|
||||
// Draw on top of everything (like camera wireframes) so lights stay visible.
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);
|
||||
|
||||
glUseProgram(m_bboxProgram);
|
||||
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
|
||||
|
||||
pxr::UsdGeomXformCache xformCache(m_currentTime);
|
||||
|
||||
for (const auto& lpath : lightPaths) {
|
||||
pxr::UsdPrim prim = stage->GetPrimAtPath(lpath);
|
||||
if (!prim) continue;
|
||||
|
||||
pxr::GfMatrix4d l2w = xformCache.GetLocalToWorldTransform(prim);
|
||||
|
||||
bool isSelected = (selectedSet.count(lpath.GetString()) > 0);
|
||||
pxr::GfVec4f col = isSelected
|
||||
? pxr::GfVec4f(1.00f, 0.75f, 0.10f, 1.00f) // accent orange
|
||||
: pxr::GfVec4f(1.00f, 0.90f, 0.35f, 0.90f); // warm yellow
|
||||
glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);
|
||||
|
||||
std::vector<float> verts;
|
||||
BuildLightWireframeLines(prim, l2w, scale, verts);
|
||||
if (verts.empty()) continue;
|
||||
|
||||
int vertCount = static_cast<int>(verts.size() / 3);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
|
||||
verts.data(), GL_DYNAMIC_DRAW);
|
||||
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
|
||||
glBindVertexArray(m_lightWireVAO);
|
||||
glDrawArrays(GL_LINES, 0, vertCount);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
|
||||
if (prevDepthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
|
||||
glDepthMask(prevDepthMask);
|
||||
if (!prevBlend) glDisable(GL_BLEND);
|
||||
|
||||
glUseProgram(0);
|
||||
m_drawTarget->Unbind();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// PickCameraAtPoint
|
||||
// ===========================================================================
|
||||
@@ -1430,7 +1881,7 @@ bool UsdSceneRenderer::PickCameraAtPoint(
|
||||
if (!prim) continue;
|
||||
|
||||
pxr::UsdGeomCamera usdCam(prim);
|
||||
pxr::GfCamera gfCam = usdCam.GetCamera(pxr::UsdTimeCode::Default());
|
||||
pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);
|
||||
|
||||
std::vector<float> verts;
|
||||
BuildCameraWireframeLines(gfCam, scale, verts);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <glad/gl.h>
|
||||
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/usd/timeCode.h>
|
||||
#include <pxr/imaging/glf/drawTarget.h>
|
||||
#include <pxr/usdImaging/usdImagingGL/engine.h>
|
||||
#include <pxr/usdImaging/usdImagingGL/rendererSettings.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <pxr/imaging/glf/simpleLight.h>
|
||||
#include <pxr/imaging/glf/simpleMaterial.h>
|
||||
#include <pxr/base/gf/vec3f.h>
|
||||
@@ -20,6 +23,7 @@
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
@@ -30,6 +34,31 @@ enum class BBoxMode {
|
||||
AllSelection ///< One combined box for the entire selection
|
||||
};
|
||||
|
||||
/// Surface shading mode — maps to UsdImagingGLDrawMode + enableLighting.
|
||||
enum class ShadingMode {
|
||||
SmoothShaded, ///< Smooth shading with materials (default)
|
||||
FlatShaded, ///< Flat shading with materials
|
||||
WireframeOnSurface, ///< Wireframe overlay on solid surface
|
||||
Wireframe, ///< Wireframe only, no fill
|
||||
Unlit, ///< Smooth geometry, lighting disabled
|
||||
};
|
||||
|
||||
/// Back-face culling style — maps directly to UsdImagingGLCullStyle.
|
||||
enum class CullStyle {
|
||||
NoOpinion = 0,
|
||||
Nothing = 1,
|
||||
Back = 2,
|
||||
Front = 3,
|
||||
BackUnlessDoubleSided = 4,
|
||||
};
|
||||
|
||||
/// Viewport color correction mode — maps to UsdImagingGLRenderParams::colorCorrectionMode.
|
||||
enum class ColorCorrectionMode {
|
||||
Disabled, ///< No correction (raw linear output)
|
||||
sRGB, ///< Linear → sRGB gamma (default)
|
||||
OpenColorIO, ///< Full OCIO pipeline via bundled ACES 1.2 config
|
||||
};
|
||||
|
||||
class UsdSceneRenderer {
|
||||
public:
|
||||
UsdSceneRenderer();
|
||||
@@ -107,6 +136,16 @@ public:
|
||||
const pxr::GfMatrix4d& viewProjMatrix,
|
||||
double viewportCameraDist);
|
||||
|
||||
/// Draw wireframe gizmos for all UsdLux light prims in the stage.
|
||||
/// The shape reflects each light's type (sphere/rect/disk/distant/dome/
|
||||
/// cylinder); selected lights get an accent colour. viewportCameraDist
|
||||
/// scales direction lines and point/distant/dome markers.
|
||||
void DrawLightWireframes(
|
||||
pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPathVector& selectedPaths,
|
||||
const pxr::GfMatrix4d& viewProjMatrix,
|
||||
double viewportCameraDist);
|
||||
|
||||
/// Test a screen-space mouse position against all camera wireframe segments.
|
||||
/// Returns true and sets *outCameraPath if a camera wireframe is within
|
||||
/// kCameraPickRadius (10 px) of mousePos. Prioritises the closest hit.
|
||||
@@ -146,12 +185,42 @@ public:
|
||||
/// unknown or the switch fails. The renderer is re-initialised if needed.
|
||||
bool SetRendererPlugin(const pxr::TfToken& pluginId);
|
||||
|
||||
/// False while a progressive delegate (Arnold/Cycles/Embree…) is still
|
||||
/// accumulating samples for the last-submitted frame; Storm is always
|
||||
/// converged after one pass. Callers that cache the output texture must
|
||||
/// keep calling Render() until this is true.
|
||||
bool IsConverged() const { return !m_renderer || m_renderer->IsConverged(); }
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// View settings
|
||||
// -----------------------------------------------------------------------
|
||||
bool ShowGrid() const { return m_showGrid; }
|
||||
void SetShowGrid(bool show) { m_showGrid = show; }
|
||||
|
||||
/// Hydra refinement complexity (1.0 = base mesh, up to 2.0). The shader
|
||||
/// ball uses a raised value so the sphere tessellates smoothly.
|
||||
float Complexity() const { return m_complexity; }
|
||||
void SetComplexity(float c) { m_complexity = c; }
|
||||
|
||||
bool ShowCameraGuide() const { return m_showCameraGuide; }
|
||||
void SetShowCameraGuide(bool show) { m_showCameraGuide = show; }
|
||||
|
||||
bool ShowGuides() const { return m_showGuides; }
|
||||
void SetShowGuides(bool show) { m_showGuides = show; }
|
||||
bool ShowProxy() const { return m_showProxy; }
|
||||
void SetShowProxy(bool show) { m_showProxy = show; }
|
||||
bool ShowRender() const { return m_showRender; }
|
||||
void SetShowRender(bool show) { m_showRender = show; }
|
||||
|
||||
const pxr::TfToken& GetCurrentAov() const { return m_currentAov; }
|
||||
pxr::TfTokenVector GetRendererAovs() const;
|
||||
bool SetCurrentAov(const pxr::TfToken& aov);
|
||||
|
||||
pxr::UsdImagingGLRendererSettingsList GetRendererSettingsList() const;
|
||||
pxr::VtValue GetRendererSetting(const pxr::TfToken& id) const;
|
||||
void SetRendererSetting(const pxr::TfToken& id,
|
||||
const pxr::VtValue& value);
|
||||
|
||||
bool GetAAEnabled() const { return m_aaEnabled; }
|
||||
void SetAAEnabled(bool enabled) { m_aaEnabled = enabled; }
|
||||
|
||||
@@ -163,6 +232,11 @@ public:
|
||||
if (val) m_cameraCacheDirty = true;
|
||||
}
|
||||
|
||||
/// Current evaluation time (timeline frame). Drives Hydra rendering,
|
||||
/// USD camera resolution, and bounding-box computation.
|
||||
pxr::UsdTimeCode GetCurrentTimeCode() const { return m_currentTime; }
|
||||
void SetCurrentTimeCode(pxr::UsdTimeCode t) { m_currentTime = t; }
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Bounding box display
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -183,6 +257,24 @@ public:
|
||||
bool GetDomeLightEnabled() const { return m_domeLightEnabled; }
|
||||
void SetDomeLightEnabled(bool val) { m_domeLightEnabled = val; }
|
||||
|
||||
ShadingMode GetShadingMode() const { return m_shadingMode; }
|
||||
void SetShadingMode(ShadingMode m) { m_shadingMode = m; m_forceRefresh = true; }
|
||||
|
||||
CullStyle GetCullStyle() const { return m_cullStyle; }
|
||||
void SetCullStyle(CullStyle c) { m_cullStyle = c; m_forceRefresh = true; }
|
||||
|
||||
ColorCorrectionMode GetColorCorrectionMode() const { return m_colorCorrectionMode; }
|
||||
void SetColorCorrectionMode(ColorCorrectionMode m) { m_colorCorrectionMode = m; m_forceRefresh = true; }
|
||||
|
||||
const std::string& GetOcioDisplay() const { return m_ocioDisplay; }
|
||||
void SetOcioDisplay(std::string v) { m_ocioDisplay = std::move(v); m_forceRefresh = true; }
|
||||
const std::string& GetOcioView() const { return m_ocioView; }
|
||||
void SetOcioView(std::string v) { m_ocioView = std::move(v); m_forceRefresh = true; }
|
||||
const std::string& GetOcioColorSpace() const { return m_ocioColorSpace; }
|
||||
void SetOcioColorSpace(std::string v){ m_ocioColorSpace = std::move(v); m_forceRefresh = true; }
|
||||
const std::string& GetOcioLook() const { return m_ocioLook; }
|
||||
void SetOcioLook(std::string v) { m_ocioLook = std::move(v); m_forceRefresh = true; }
|
||||
|
||||
/// Default material ambient (kA, default 0.2 — matches viewSettingsDataModel.py).
|
||||
float GetDefaultMaterialAmbient() const { return m_defaultMaterialAmbient; }
|
||||
void SetDefaultMaterialAmbient(float v) { m_defaultMaterialAmbient = v; }
|
||||
@@ -196,11 +288,22 @@ public:
|
||||
// -----------------------------------------------------------------------
|
||||
uint32_t GetColorTextureID();
|
||||
|
||||
/// Dimensions of the most recent Render() call.
|
||||
int GetLastRenderWidth() const { return m_lastRenderWidth; }
|
||||
int GetLastRenderHeight() const { return m_lastRenderHeight; }
|
||||
|
||||
/// Read the last rendered frame into top-down RGBA8 pixels.
|
||||
/// Resolves MSAA before reading. Returns false if the draw target is not ready.
|
||||
bool CaptureFrame(std::vector<uint8_t>& outRGBA);
|
||||
|
||||
// For unit tests
|
||||
pxr::GlfDrawTargetRefPtr GetDrawTargetForTest() const { return m_drawTarget; }
|
||||
|
||||
private:
|
||||
int m_lastRenderWidth = 0;
|
||||
int m_lastRenderHeight = 0;
|
||||
void InitRenderer();
|
||||
|
||||
void InitGridResources();
|
||||
void RebuildGridVBO(); ///< (Re)build grid line geometry after up-axis or size change.
|
||||
void DestroyGridResources();
|
||||
@@ -211,6 +314,8 @@ private:
|
||||
void DestroyBBoxResources();
|
||||
void InitCamWireResources();
|
||||
void DestroyCamWireResources();
|
||||
void InitLightWireResources();
|
||||
void DestroyLightWireResources();
|
||||
/// Draw a single axis-aligned box from a GfRange3d.
|
||||
void DrawBox(const pxr::GfRange3d& range, const pxr::GfMatrix4f& mvp);
|
||||
|
||||
@@ -221,6 +326,18 @@ private:
|
||||
double scale,
|
||||
std::vector<float>& outVerts);
|
||||
|
||||
/// Build light-gizmo line segments (GL_LINES vertex pairs) in world space for
|
||||
/// a single light prim. localToWorld is the light's transform at m_currentTime;
|
||||
/// scale is the display-size factor (for direction lines / point markers).
|
||||
void BuildLightWireframeLines(const pxr::UsdPrim& lightPrim,
|
||||
const pxr::GfMatrix4d& localToWorld,
|
||||
double scale,
|
||||
std::vector<float>& outVerts);
|
||||
|
||||
/// True if the stage contains at least one authored UsdLux light.
|
||||
/// Used to suppress the default camera headlight when real lights exist.
|
||||
bool StageHasAuthoredLights() const;
|
||||
|
||||
/// Project a world-space point to absolute screen coordinates (x=imagePosX+pixelX, etc.).
|
||||
/// Returns false when the point is behind the camera.
|
||||
static bool WorldToScreen(const pxr::GfVec3d& world,
|
||||
@@ -236,7 +353,7 @@ private:
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
std::shared_ptr<pxr::UsdImagingGLEngine> m_renderer;
|
||||
pxr::TfToken m_currentRendererPlugin; ///< Active plugin ID (empty = default)
|
||||
pxr::TfToken m_currentRendererPlugin; ///< empty = use Storm/GL heuristic on init
|
||||
pxr::GlfDrawTargetRefPtr m_drawTarget;
|
||||
|
||||
pxr::UsdImagingGLRenderParams m_renderParams;
|
||||
@@ -254,10 +371,23 @@ private:
|
||||
std::vector<pxr::GfVec4d> m_clipPlanes;
|
||||
|
||||
bool m_showGrid;
|
||||
float m_complexity = 1.0f;
|
||||
bool m_showCameraGuide;
|
||||
bool m_showGuides;
|
||||
bool m_showProxy;
|
||||
bool m_showRender;
|
||||
pxr::TfToken m_currentAov;
|
||||
bool m_aaEnabled; ///< GL_LINE_SMOOTH anti-aliasing for all line overlays
|
||||
pxr::GfVec3f m_backgroundColor;
|
||||
|
||||
// Lighting settings (mirrors stageView.py viewSettings)
|
||||
ShadingMode m_shadingMode; // draw mode + lighting flags
|
||||
CullStyle m_cullStyle; // back/front/none face culling
|
||||
ColorCorrectionMode m_colorCorrectionMode; // viewport color correction
|
||||
std::string m_ocioDisplay;
|
||||
std::string m_ocioView;
|
||||
std::string m_ocioColorSpace;
|
||||
std::string m_ocioLook;
|
||||
bool m_ambientLightOnly; // camera headlight
|
||||
bool m_domeLightEnabled; // dome/IBL light
|
||||
bool m_stageIsZup; // used for dome light rotation
|
||||
@@ -267,6 +397,8 @@ private:
|
||||
bool m_rendererInitialized;
|
||||
bool m_forceRefresh;
|
||||
|
||||
pxr::UsdTimeCode m_currentTime = pxr::UsdTimeCode::Default();
|
||||
|
||||
int m_diagFrameCount;
|
||||
|
||||
// --- Grid GL resources (line geometry, reuses m_axisProgram / m_axisUniformMVP) ---
|
||||
@@ -299,9 +431,14 @@ private:
|
||||
GLuint m_camWireVAO = 0;
|
||||
GLuint m_camWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed
|
||||
|
||||
// --- Light wireframe GL resources (mirrors camera wireframe) ---
|
||||
GLuint m_lightWireVAO = 0;
|
||||
GLuint m_lightWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed
|
||||
|
||||
// --- Camera wireframe cache ---
|
||||
pxr::SdfPathVector m_cachedCameraPaths;
|
||||
bool m_cameraCacheDirty = true;
|
||||
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
@@ -134,6 +134,12 @@ bool UsdStageManager::SaveStage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
|
||||
if (!rootLayer || rootLayer->IsAnonymous()) {
|
||||
SetError("Stage has no file path — use Save As to save to disk");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LOG_INFO("Saving USD stage");
|
||||
|
||||
@@ -142,6 +148,13 @@ bool UsdStageManager::SaveStage() {
|
||||
// cannot be saved by UsdStage::Save() itself).
|
||||
MergeSessionLayerIntoRoot(m_stage);
|
||||
|
||||
// Explicitly save the root layer first. UsdStage::Save() iterates
|
||||
// the PCP composition stack and saves dirty layers, but sublayer-path
|
||||
// edits (InsertSubLayerPath) are root-layer metadata changes that may
|
||||
// not be flushed by stage-level Save() in all USD versions.
|
||||
rootLayer->Save();
|
||||
|
||||
// Save any other dirty sublayers that have file paths.
|
||||
m_stage->Save();
|
||||
LOG_INFO("Successfully saved USD stage");
|
||||
return true;
|
||||
|
||||
@@ -335,6 +335,29 @@ void ViewportCamera::SetUsdCamera(const pxr::SdfPath& cameraPath)
|
||||
m_usdCameraPath = cameraPath;
|
||||
}
|
||||
|
||||
void ViewportCamera::InitOrbitFromEyeAndCenter(
|
||||
const pxr::GfVec3d& eye, const pxr::GfVec3d& center, double dist)
|
||||
{
|
||||
m_center = center;
|
||||
m_dist = std::max(dist, 0.001);
|
||||
m_selSize = m_dist / 10.0;
|
||||
|
||||
// Transform the eye-to-center offset into the Y-up orbital frame.
|
||||
// For Y-up stages m_YZUpMatrix is identity; for Z-up it's -90° around X.
|
||||
pxr::GfVec3d offset = m_YZUpMatrix.TransformDir(eye - center);
|
||||
double len = offset.GetLength();
|
||||
if (len > 1e-6) {
|
||||
offset /= len;
|
||||
// In the Y-up orbital frame PushToCameraTransform places the eye at:
|
||||
// (-sin(theta)*cos(phi), sin(phi), cos(theta)*cos(phi))
|
||||
m_rotPhi = std::asin(std::max(-1.0, std::min(1.0, offset[1])))
|
||||
* (180.0 / M_PI);
|
||||
m_rotTheta = std::atan2(-offset[0], offset[2]) * (180.0 / M_PI);
|
||||
}
|
||||
m_rotPsi = 0.0; // zero roll — avoids gimbal weirdness on switch
|
||||
m_cameraTransformDirty = true;
|
||||
}
|
||||
|
||||
void ViewportCamera::SwitchToFreeCamera(const pxr::GfCamera* lastGfCamera)
|
||||
{
|
||||
if (m_mode == CameraMode::Free) return;
|
||||
|
||||
@@ -113,6 +113,22 @@ public:
|
||||
m_cameraTransformDirty = true;
|
||||
}
|
||||
|
||||
/// Set the orbit distance directly.
|
||||
/// Used when initialising from a USD camera prim to override the camera's
|
||||
/// authored focus distance (which is a DOF attribute, not an orbit radius).
|
||||
void SetDist(double d) {
|
||||
m_dist = d;
|
||||
m_cameraTransformDirty = true;
|
||||
}
|
||||
|
||||
/// Set orbital state from eye position and orbit center without Euler decomposition.
|
||||
/// Computes theta/phi directly from the eye→center vector in the Y-up orbital frame;
|
||||
/// zeroes roll. Use this after SwitchToFreeCamera() when initialising from a USD
|
||||
/// camera prim to avoid gimbal-lock artifacts from the Decompose() path.
|
||||
void InitOrbitFromEyeAndCenter(const pxr::GfVec3d& eye,
|
||||
const pxr::GfVec3d& center,
|
||||
double dist);
|
||||
|
||||
/// Returns the current near-clip distance.
|
||||
double GetNearClip() const {
|
||||
return static_cast<double>(m_camera.GetClippingRange().GetMin());
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#include "ConnectShaderAttrsCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/editContext.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& destNode,
|
||||
const std::string& destInput,
|
||||
const pxr::SdfValueTypeName& destType,
|
||||
const pxr::SdfPath& sourceNode,
|
||||
const std::string& sourceOutput,
|
||||
const pxr::SdfValueTypeName& sourceType,
|
||||
pxr::SdfLayerHandle editLayer)
|
||||
: m_stage(stage)
|
||||
, m_destNode(destNode)
|
||||
, m_destInput(destInput)
|
||||
, m_destType(destType)
|
||||
, m_sourceNode(sourceNode)
|
||||
, m_sourceOutput(sourceOutput)
|
||||
, m_sourceType(sourceType)
|
||||
, m_editLayer(editLayer)
|
||||
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
|
||||
" -> " + destNode.GetName() + "." + destInput)
|
||||
{
|
||||
if (!stage) return;
|
||||
|
||||
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
|
||||
if (!destShader) return;
|
||||
|
||||
pxr::UsdShadeInput existingInput = destShader.GetInput(pxr::TfToken(destInput));
|
||||
if (!existingInput) return;
|
||||
|
||||
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(existingInput)) {
|
||||
if (!source.IsValid()) continue;
|
||||
m_hadPriorConnection = true;
|
||||
m_priorSourceNode = source.source.GetPrim().GetPath();
|
||||
m_priorSourceOutput = source.sourceName.GetString();
|
||||
break; // shader inputs carry a single upstream connection in this editor's model
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectShaderAttrsCommand::Execute() {
|
||||
if (!m_stage) return;
|
||||
try {
|
||||
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
|
||||
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
|
||||
if (!destShader || !sourceShader) {
|
||||
LOG_ERROR("ConnectShaderAttrsCommand: invalid shader prim(s)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
|
||||
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
|
||||
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
|
||||
} else {
|
||||
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
|
||||
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
|
||||
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectShaderAttrsCommand::Undo() {
|
||||
if (!m_stage) return;
|
||||
try {
|
||||
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
|
||||
if (!destShader) return;
|
||||
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||
if (!destInput) return;
|
||||
|
||||
auto doUndo = [&]() {
|
||||
if (m_hadPriorConnection) {
|
||||
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
|
||||
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
|
||||
if (priorSourceShader && priorSourceOutput)
|
||||
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput);
|
||||
} else {
|
||||
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
doUndo();
|
||||
} else {
|
||||
doUndo();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/valueTypeName.h>
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Connects a shader input to an upstream shader output, sparsely authoring
|
||||
/// both attributes (with the given types) if they don't already exist.
|
||||
/// Captures whatever the input was previously connected to (if anything) so
|
||||
/// Undo can restore it exactly rather than merely disconnecting.
|
||||
class ConnectShaderAttrsCommand : public ICommand {
|
||||
public:
|
||||
/// editLayer: layer to author into (mirrors TransformCommand's pattern).
|
||||
/// Captured at construction time so Undo() targets the same layer as
|
||||
/// Execute() even if the stage's ambient edit target changes in between
|
||||
/// (e.g. a render-layer switch) — pass null/empty to fall back to
|
||||
/// whatever the ambient edit target is at call time (today's behavior).
|
||||
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& destNode,
|
||||
const std::string& destInput,
|
||||
const pxr::SdfValueTypeName& destType,
|
||||
const pxr::SdfPath& sourceNode,
|
||||
const std::string& sourceOutput,
|
||||
const pxr::SdfValueTypeName& sourceType,
|
||||
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_destNode;
|
||||
std::string m_destInput;
|
||||
pxr::SdfValueTypeName m_destType;
|
||||
pxr::SdfPath m_sourceNode;
|
||||
std::string m_sourceOutput;
|
||||
pxr::SdfValueTypeName m_sourceType;
|
||||
pxr::SdfLayerHandle m_editLayer;
|
||||
std::string m_description;
|
||||
|
||||
bool m_hadPriorConnection = false;
|
||||
pxr::SdfPath m_priorSourceNode;
|
||||
std::string m_priorSourceOutput;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "CreateShaderNodeCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
CreateShaderNodeCommand::CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& path,
|
||||
const std::string& shaderId,
|
||||
const pxr::GfVec2f& uiPosition)
|
||||
: m_stage(stage)
|
||||
, m_path(path)
|
||||
, m_shaderId(shaderId)
|
||||
, m_uiPosition(uiPosition)
|
||||
, m_description("Create Shader " + shaderId + " " + path.GetString())
|
||||
{
|
||||
}
|
||||
|
||||
void CreateShaderNodeCommand::Execute() {
|
||||
if (!m_stage) return;
|
||||
try {
|
||||
pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(m_stage, m_path);
|
||||
if (!shader) {
|
||||
LOG_ERROR("CreateShaderNodeCommand: failed to define shader " + m_path.GetString());
|
||||
return;
|
||||
}
|
||||
shader.SetShaderId(pxr::TfToken(m_shaderId));
|
||||
shader.GetPrim().SetCustomDataByKey(pxr::TfToken("uiPosition"), pxr::VtValue(m_uiPosition));
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("CreateShaderNodeCommand::Execute error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void CreateShaderNodeCommand::Undo() {
|
||||
if (!m_stage) return;
|
||||
try {
|
||||
if (!m_stage->RemovePrim(m_path))
|
||||
LOG_ERROR("CreateShaderNodeCommand: failed to remove prim " + m_path.GetString());
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("CreateShaderNodeCommand::Undo error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/base/gf/vec2f.h>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Creates a UsdShadeShader prim with the given Sdr shader identifier and
|
||||
/// authors its canvas position as uiPosition custom data (see
|
||||
/// docs/adr — Material Editor node-graph layout is stored as prim
|
||||
/// customData, not scene data, so it never affects composition/renders).
|
||||
class CreateShaderNodeCommand : public ICommand {
|
||||
public:
|
||||
CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& path,
|
||||
const std::string& shaderId,
|
||||
const pxr::GfVec2f& uiPosition);
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_path;
|
||||
std::string m_shaderId;
|
||||
pxr::GfVec2f m_uiPosition;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -59,6 +59,14 @@ void DeletePrimCommand::Undo() {
|
||||
if (!rootLayer) return;
|
||||
|
||||
try {
|
||||
// The destination ancestors may be gone by now (e.g. the parent
|
||||
// material was itself deleted after this command ran) — SdfCopySpec
|
||||
// fails with "No spec ... when trying to set field 'primChildren'"
|
||||
// when the parent spec is missing, so recreate the chain first.
|
||||
pxr::SdfPath parentPath = m_primPath.GetParentPath();
|
||||
if (!parentPath.IsAbsoluteRootPath() && !rootLayer->GetPrimAtPath(parentPath))
|
||||
pxr::SdfCreatePrimInLayer(rootLayer, parentPath);
|
||||
|
||||
if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath))
|
||||
LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString());
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "DisconnectShaderAttrCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/editContext.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& destNode,
|
||||
const std::string& destInput,
|
||||
pxr::SdfLayerHandle editLayer)
|
||||
: m_stage(stage)
|
||||
, m_destNode(destNode)
|
||||
, m_destInput(destInput)
|
||||
, m_editLayer(editLayer)
|
||||
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
|
||||
{
|
||||
if (!stage) return;
|
||||
|
||||
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
|
||||
if (!destShader) return;
|
||||
|
||||
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(destInput));
|
||||
if (!input) return;
|
||||
|
||||
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(input)) {
|
||||
if (!source.IsValid()) continue;
|
||||
m_hadConnection = true;
|
||||
m_sourceNode = source.source.GetPrim().GetPath();
|
||||
m_sourceOutput = source.sourceName.GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DisconnectShaderAttrCommand::Execute() {
|
||||
if (!m_stage) return;
|
||||
try {
|
||||
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
|
||||
if (!destShader) return;
|
||||
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||
if (!input) return;
|
||||
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
|
||||
} else {
|
||||
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void DisconnectShaderAttrCommand::Undo() {
|
||||
if (!m_stage || !m_hadConnection) return;
|
||||
try {
|
||||
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
|
||||
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
|
||||
if (!destShader || !sourceShader) return;
|
||||
|
||||
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
|
||||
if (!input || !output) return;
|
||||
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
|
||||
} else {
|
||||
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Disconnects a shader input from its upstream source, capturing that
|
||||
/// source at construction time so Undo can restore it.
|
||||
class DisconnectShaderAttrCommand : public ICommand {
|
||||
public:
|
||||
/// editLayer: see ConnectShaderAttrsCommand's constructor doc — same
|
||||
/// capture-at-construction-time pattern, null/empty falls back to the
|
||||
/// ambient edit target at call time (today's behavior).
|
||||
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& destNode,
|
||||
const std::string& destInput,
|
||||
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_destNode;
|
||||
std::string m_destInput;
|
||||
pxr::SdfLayerHandle m_editLayer;
|
||||
std::string m_description;
|
||||
|
||||
bool m_hadConnection = false;
|
||||
pxr::SdfPath m_sourceNode;
|
||||
std::string m_sourceOutput;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "GroupPrimsCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
GroupPrimsCommand::GroupPrimsCommand(UsdStageRefPtr stage,
|
||||
const std::vector<SdfPath>& srcPaths,
|
||||
const SdfPath& groupPath)
|
||||
: m_stage(stage)
|
||||
, m_groupPath(groupPath)
|
||||
, m_srcPaths(srcPaths)
|
||||
, m_description("Group " + std::to_string(srcPaths.size()) +
|
||||
" prim(s) under " + groupPath.GetString())
|
||||
{
|
||||
}
|
||||
|
||||
/// Compute a unique child name under @p parent (collision check against live stage).
|
||||
static std::string UniqueNameUnder(UsdStageRefPtr stage,
|
||||
const SdfPath& parent,
|
||||
const std::string& baseName) {
|
||||
std::string name = baseName;
|
||||
int n = 1;
|
||||
while (stage->GetPrimAtPath(parent.AppendChild(TfToken(name))).IsValid())
|
||||
name = baseName + "_" + std::to_string(n++);
|
||||
return name;
|
||||
}
|
||||
|
||||
static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) {
|
||||
if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath())
|
||||
return stage->GetPseudoRoot();
|
||||
return stage->GetPrimAtPath(parentPath);
|
||||
}
|
||||
|
||||
void GroupPrimsCommand::Execute() {
|
||||
// Create the Xform group.
|
||||
UsdPrim groupPrim = m_stage->DefinePrim(m_groupPath, TfToken("Xform"));
|
||||
if (!groupPrim.IsValid()) {
|
||||
LOG_ERROR("GroupPrimsCommand::Execute: failed to create group: " + m_groupPath.GetString());
|
||||
return;
|
||||
}
|
||||
|
||||
m_movedNames.clear();
|
||||
|
||||
for (const SdfPath& srcPath : m_srcPaths) {
|
||||
UsdPrim src = m_stage->GetPrimAtPath(srcPath);
|
||||
if (!src.IsValid()) {
|
||||
LOG_WARNING("GroupPrimsCommand::Execute: prim not found (skipping): " + srcPath.GetString());
|
||||
m_movedNames.emplace_back(srcPath.GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute a collision-free name inside the group.
|
||||
std::string name = UniqueNameUnder(m_stage, m_groupPath, srcPath.GetName());
|
||||
m_movedNames.push_back(name);
|
||||
|
||||
UsdPrim gPrim = m_stage->GetPrimAtPath(m_groupPath);
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
editor.ReparentPrim(src, gPrim, TfToken(name));
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("GroupPrimsCommand::Execute: cannot reparent " + srcPath.GetString() + ": " + whyNot);
|
||||
continue;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("GroupPrimsCommand::Execute: ApplyEdits failed for " + srcPath.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupPrimsCommand::Undo() {
|
||||
if (m_movedNames.size() != m_srcPaths.size()) {
|
||||
LOG_ERROR("GroupPrimsCommand::Undo: state mismatch, cannot undo safely");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reparent children back in reverse order.
|
||||
for (int i = (int)m_srcPaths.size() - 1; i >= 0; --i) {
|
||||
SdfPath movedPath = m_groupPath.AppendChild(TfToken(m_movedNames[i]));
|
||||
UsdPrim moved = m_stage->GetPrimAtPath(movedPath);
|
||||
if (!moved.IsValid()) {
|
||||
LOG_WARNING("GroupPrimsCommand::Undo: prim not in group (skipping): " + movedPath.GetString());
|
||||
continue;
|
||||
}
|
||||
|
||||
SdfPath origParentPath = m_srcPaths[i].GetParentPath();
|
||||
UsdPrim origParent = GetParentPrim(m_stage, origParentPath);
|
||||
if (!origParent.IsValid()) {
|
||||
LOG_ERROR("GroupPrimsCommand::Undo: original parent not found: " + origParentPath.GetString());
|
||||
continue;
|
||||
}
|
||||
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
editor.ReparentPrim(moved, origParent, TfToken(m_srcPaths[i].GetName()));
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("GroupPrimsCommand::Undo: cannot reparent back " + movedPath.GetString() + ": " + whyNot);
|
||||
continue;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("GroupPrimsCommand::Undo: ApplyEdits failed for " + movedPath.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the (now empty) group prim.
|
||||
m_stage->RemovePrim(m_groupPath);
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Creates a new Xform group prim and reparents the given prims under it.
|
||||
/// The group path must be pre-computed (collision-free) by the caller.
|
||||
/// Undo reparents each child back to its original parent and removes the group.
|
||||
class GroupPrimsCommand : public ICommand {
|
||||
public:
|
||||
GroupPrimsCommand(pxr::UsdStageRefPtr stage,
|
||||
const std::vector<pxr::SdfPath>& srcPaths,
|
||||
const pxr::SdfPath& groupPath);
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_groupPath;
|
||||
std::vector<pxr::SdfPath> m_srcPaths; ///< original prim paths (before group)
|
||||
std::vector<std::string> m_movedNames; ///< actual names used inside group (set in Execute)
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -15,15 +15,15 @@ LayerCreateCommand::LayerCreateCommand(LayerManager* mgr, std::string path, int
|
||||
|
||||
void LayerCreateCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
// Record how many sublayers exist before insert to compute the real index used.
|
||||
auto before = m_mgr->GetLayerStack().size();
|
||||
auto before = m_mgr->GetSublayers().size();
|
||||
m_mgr->CreateSublayer(m_path, m_index);
|
||||
auto after = m_mgr->GetLayerStack().size();
|
||||
// If a layer was actually inserted, compute its index.
|
||||
if (after > before) {
|
||||
auto layers = m_mgr->GetLayerStack();
|
||||
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
|
||||
if (layers[i].identifier == m_path || layers[i].displayName == m_path) {
|
||||
// Find the sublayer-local index so Undo() can call RemoveSublayer() correctly.
|
||||
// RemoveSublayer() uses indices into GetSubLayerPaths() (0..N-1), not the full
|
||||
// layer stack that includes session and root layers.
|
||||
auto sublayers = m_mgr->GetSublayers();
|
||||
if (sublayers.size() > before) {
|
||||
for (int i = 0; i < static_cast<int>(sublayers.size()); ++i) {
|
||||
if (sublayers[i].identifier == m_path) {
|
||||
m_insertedIndex = i;
|
||||
break;
|
||||
}
|
||||
@@ -46,10 +46,10 @@ LayerRemoveCommand::LayerRemoveCommand(LayerManager* mgr, int index)
|
||||
, m_description("Remove Layer")
|
||||
{
|
||||
if (!mgr) return;
|
||||
auto layers = mgr->GetLayerStack();
|
||||
if (index >= 0 && index < static_cast<int>(layers.size())) {
|
||||
m_savedPath = layers[index].identifier;
|
||||
m_description = "Remove Layer " + layers[index].displayName;
|
||||
auto sublayers = mgr->GetSublayers();
|
||||
if (index >= 0 && index < static_cast<int>(sublayers.size())) {
|
||||
m_savedPath = sublayers[index].identifier;
|
||||
m_description = "Remove Layer " + sublayers[index].displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,22 +81,14 @@ void LayerReorderCommand::Undo() { ApplyOrder(m_before); }
|
||||
|
||||
void LayerReorderCommand::ApplyOrder(const std::vector<std::string>& order) {
|
||||
if (!m_mgr) return;
|
||||
// Remove all sublayers and re-insert in the desired order.
|
||||
// We only control sublayers — root and session are fixed.
|
||||
// First gather current sublayer indices (non-root, non-session).
|
||||
auto layers = m_mgr->GetLayerStack();
|
||||
|
||||
// Count sublayers and remove them from highest index down.
|
||||
std::vector<int> sublayerIndices;
|
||||
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
|
||||
if (!layers[i].isRootLayer && !layers[i].isSessionLayer)
|
||||
sublayerIndices.push_back(i);
|
||||
}
|
||||
// Remove from back to front to preserve indices.
|
||||
for (int i = static_cast<int>(sublayerIndices.size()) - 1; i >= 0; --i)
|
||||
m_mgr->RemoveSublayer(sublayerIndices[i]);
|
||||
// RemoveSublayer() operates on GetSubLayerPaths() — a sublayer-local list
|
||||
// with indices 0..N-1. GetLayerStack() prepends session and root layers, so
|
||||
// its indices are always wrong here. Use GetSublayers().size() for the count.
|
||||
int count = static_cast<int>(m_mgr->GetSublayers().size());
|
||||
for (int i = count - 1; i >= 0; --i)
|
||||
m_mgr->RemoveSublayer(i);
|
||||
|
||||
// Re-add in desired order.
|
||||
for (const auto& path : order)
|
||||
m_mgr->InsertSublayerPath(path, -1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "RenamePrimCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
RenamePrimCommand::RenamePrimCommand(UsdStageRefPtr stage,
|
||||
const SdfPath& primPath,
|
||||
const std::string& newName)
|
||||
: m_stage(stage)
|
||||
, m_oldPath(primPath)
|
||||
, m_oldName(primPath.GetName())
|
||||
, m_newName(newName)
|
||||
, m_description("Rename " + primPath.GetName() + " \xe2\x86\x92 " + newName)
|
||||
{
|
||||
m_newPath = primPath.GetParentPath().AppendChild(TfToken(newName));
|
||||
}
|
||||
|
||||
void RenamePrimCommand::Execute() {
|
||||
UsdPrim prim = m_stage->GetPrimAtPath(m_oldPath);
|
||||
if (!prim.IsValid()) {
|
||||
LOG_ERROR("RenamePrimCommand::Execute: prim not found: " + m_oldPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
if (!editor.RenamePrim(prim, TfToken(m_newName))) {
|
||||
LOG_ERROR("RenamePrimCommand::Execute: RenamePrim() rejected for " + m_oldPath.GetString());
|
||||
return;
|
||||
}
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot);
|
||||
return;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("RenamePrimCommand::Execute: ApplyEdits failed for " + m_oldPath.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
void RenamePrimCommand::Undo() {
|
||||
UsdPrim prim = m_stage->GetPrimAtPath(m_newPath);
|
||||
if (!prim.IsValid()) {
|
||||
LOG_ERROR("RenamePrimCommand::Undo: prim not found: " + m_newPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
if (!editor.RenamePrim(prim, TfToken(m_oldName))) {
|
||||
LOG_ERROR("RenamePrimCommand::Undo: RenamePrim() rejected for " + m_newPath.GetString());
|
||||
return;
|
||||
}
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot);
|
||||
return;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("RenamePrimCommand::Undo: ApplyEdits failed for " + m_newPath.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Renames a USD prim via UsdNamespaceEditor, updating all composition arcs.
|
||||
/// Undo restores the original name.
|
||||
class RenamePrimCommand : public ICommand {
|
||||
public:
|
||||
RenamePrimCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& primPath,
|
||||
const std::string& newName);
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_oldPath; ///< path before rename
|
||||
pxr::SdfPath m_newPath; ///< path after rename (set in Execute)
|
||||
std::string m_oldName;
|
||||
std::string m_newName;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "RenderLayerCommands.h"
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
// ─── RenderLayerCreateCommand ─────────────────────────────────────────────
|
||||
|
||||
RenderLayerCreateCommand::RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name)
|
||||
: m_mgr(mgr)
|
||||
, m_name(std::move(name))
|
||||
, m_description("Create Render Layer " + m_name)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderLayerCreateCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
m_layerId = m_mgr->CreateRenderLayer(m_name);
|
||||
}
|
||||
|
||||
void RenderLayerCreateCommand::Undo() {
|
||||
if (!m_mgr || m_layerId.empty()) return;
|
||||
m_mgr->DeleteRenderLayer(m_layerId);
|
||||
}
|
||||
|
||||
// ─── RenderLayerDeleteCommand ─────────────────────────────────────────────
|
||||
|
||||
RenderLayerDeleteCommand::RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId)
|
||||
: m_mgr(mgr)
|
||||
, m_layerId(std::move(layerId))
|
||||
, m_description("Delete Render Layer")
|
||||
{
|
||||
if (!mgr) return;
|
||||
for (const auto& info : mgr->GetRenderLayers()) {
|
||||
if (!info.isDefault && info.layerIdentifier == m_layerId) {
|
||||
m_description = "Delete Render Layer " + info.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderLayerDeleteCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
m_mgr->DeleteRenderLayer(m_layerId);
|
||||
}
|
||||
|
||||
void RenderLayerDeleteCommand::Undo() {
|
||||
if (!m_mgr || m_layerId.empty()) return;
|
||||
m_mgr->RestoreDeletedRenderLayer(m_layerId);
|
||||
}
|
||||
|
||||
// ─── RenderLayerRenameCommand ─────────────────────────────────────────────
|
||||
|
||||
RenderLayerRenameCommand::RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId,
|
||||
std::string newName)
|
||||
: m_mgr(mgr)
|
||||
, m_layerId(std::move(layerId))
|
||||
, m_newName(std::move(newName))
|
||||
, m_description("Rename Render Layer")
|
||||
{
|
||||
if (!mgr) return;
|
||||
for (const auto& info : mgr->GetRenderLayers()) {
|
||||
if (!info.isDefault && info.layerIdentifier == m_layerId) {
|
||||
m_oldName = info.name;
|
||||
m_description = "Rename Render Layer " + info.name + " -> " + m_newName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderLayerRenameCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
m_mgr->RenameRenderLayer(m_layerId, m_newName);
|
||||
}
|
||||
|
||||
void RenderLayerRenameCommand::Undo() {
|
||||
if (!m_mgr || m_oldName.empty()) return;
|
||||
m_mgr->RenameRenderLayer(m_layerId, m_oldName);
|
||||
}
|
||||
|
||||
// ─── RenderLayerActivateCommand ───────────────────────────────────────────
|
||||
|
||||
RenderLayerActivateCommand::RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId)
|
||||
: m_mgr(mgr)
|
||||
, m_targetLayerId(std::move(targetLayerId))
|
||||
, m_description(m_targetLayerId.empty() ? "Activate Render Layer: Default"
|
||||
: "Activate Render Layer")
|
||||
{
|
||||
if (mgr) m_priorLayerId = mgr->GetActiveRenderLayerId();
|
||||
}
|
||||
|
||||
void RenderLayerActivateCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
if (m_targetLayerId.empty()) m_mgr->ActivateDefaultLayer();
|
||||
else m_mgr->ActivateRenderLayer(m_targetLayerId);
|
||||
}
|
||||
|
||||
void RenderLayerActivateCommand::Undo() {
|
||||
if (!m_mgr) return;
|
||||
if (m_priorLayerId.empty()) m_mgr->ActivateDefaultLayer();
|
||||
else m_mgr->ActivateRenderLayer(m_priorLayerId);
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include "../RenderLayerManager.h"
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Undo removes the created layer via RenderLayerManager::DeleteRenderLayer.
|
||||
class RenderLayerCreateCommand : public ICommand {
|
||||
public:
|
||||
RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
/// Valid after Execute() — the new layer's identifier, so the panel can
|
||||
/// select it immediately.
|
||||
const std::string& GetCreatedLayerId() const { return m_layerId; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_name;
|
||||
std::string m_layerId;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
/// The backing file is left on disk by RenderLayerManager::DeleteRenderLayer
|
||||
/// (only the sublayer reference is removed), so Undo just re-inserts it.
|
||||
class RenderLayerDeleteCommand : public ICommand {
|
||||
public:
|
||||
RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_layerId;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
class RenderLayerRenameCommand : public ICommand {
|
||||
public:
|
||||
RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId, std::string newName);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_layerId;
|
||||
std::string m_newName;
|
||||
std::string m_oldName;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
/// Switches the active render layer (or back to Default). RenderLayerManager
|
||||
/// itself tracks the edit target to restore on returning to Default, so this
|
||||
/// command only needs to remember which layer was active before the switch.
|
||||
class RenderLayerActivateCommand : public ICommand {
|
||||
public:
|
||||
/// targetLayerId empty = activate Default.
|
||||
RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_targetLayerId; // "" = Default
|
||||
std::string m_priorLayerId; // captured at construction; "" = Default
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "RenderLayerMembershipCommand.h"
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
// ─── RenderLayerAddMembersCommand ─────────────────────────────────────────
|
||||
|
||||
RenderLayerAddMembersCommand::RenderLayerAddMembersCommand(RenderLayerManager* mgr,
|
||||
std::string layerId,
|
||||
std::vector<pxr::SdfPath> paths)
|
||||
: m_mgr(mgr)
|
||||
, m_layerId(std::move(layerId))
|
||||
, m_paths(std::move(paths))
|
||||
, m_description(m_paths.size() == 1
|
||||
? ("Add " + m_paths.front().GetString() + " to Render Layer")
|
||||
: ("Add " + std::to_string(m_paths.size()) + " Members to Render Layer"))
|
||||
{
|
||||
}
|
||||
|
||||
void RenderLayerAddMembersCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
for (const auto& p : m_paths)
|
||||
m_mgr->AddMember(m_layerId, p);
|
||||
}
|
||||
|
||||
void RenderLayerAddMembersCommand::Undo() {
|
||||
if (!m_mgr) return;
|
||||
for (const auto& p : m_paths)
|
||||
m_mgr->RemoveMember(m_layerId, p);
|
||||
}
|
||||
|
||||
// ─── RenderLayerRemoveMembersCommand ──────────────────────────────────────
|
||||
|
||||
RenderLayerRemoveMembersCommand::RenderLayerRemoveMembersCommand(RenderLayerManager* mgr,
|
||||
std::string layerId,
|
||||
std::vector<pxr::SdfPath> paths)
|
||||
: m_mgr(mgr)
|
||||
, m_layerId(std::move(layerId))
|
||||
, m_paths(std::move(paths))
|
||||
, m_description(m_paths.size() == 1
|
||||
? ("Remove " + m_paths.front().GetString() + " from Render Layer")
|
||||
: ("Remove " + std::to_string(m_paths.size()) + " Members from Render Layer"))
|
||||
{
|
||||
}
|
||||
|
||||
void RenderLayerRemoveMembersCommand::Execute() {
|
||||
if (!m_mgr) return;
|
||||
for (const auto& p : m_paths)
|
||||
m_mgr->RemoveMember(m_layerId, p);
|
||||
}
|
||||
|
||||
void RenderLayerRemoveMembersCommand::Undo() {
|
||||
if (!m_mgr) return;
|
||||
for (const auto& p : m_paths)
|
||||
m_mgr->AddMember(m_layerId, p);
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include "../RenderLayerManager.h"
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Adds one or more prim paths to a render layer's membership (Sdf-level, so
|
||||
/// this works whether or not layerId is currently active). Re-runs viewport
|
||||
/// isolation if layerId happens to be the active layer.
|
||||
class RenderLayerAddMembersCommand : public ICommand {
|
||||
public:
|
||||
RenderLayerAddMembersCommand(RenderLayerManager* mgr, std::string layerId,
|
||||
std::vector<pxr::SdfPath> paths);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_layerId;
|
||||
std::vector<pxr::SdfPath> m_paths;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
class RenderLayerRemoveMembersCommand : public ICommand {
|
||||
public:
|
||||
RenderLayerRemoveMembersCommand(RenderLayerManager* mgr, std::string layerId,
|
||||
std::vector<pxr::SdfPath> paths);
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
RenderLayerManager* m_mgr;
|
||||
std::string m_layerId;
|
||||
std::vector<pxr::SdfPath> m_paths;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "ReparentPrimCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
ReparentPrimCommand::ReparentPrimCommand(UsdStageRefPtr stage,
|
||||
const SdfPath& srcPath,
|
||||
const SdfPath& dstParentPath,
|
||||
const std::string& finalName)
|
||||
: m_stage(stage)
|
||||
, m_srcPath(srcPath)
|
||||
, m_origParentPath(srcPath.GetParentPath())
|
||||
, m_origName(srcPath.GetName())
|
||||
, m_dstParentPath(dstParentPath)
|
||||
, m_finalName(finalName)
|
||||
, m_description("Reparent " + srcPath.GetString() + " \xe2\x86\x92 " +
|
||||
dstParentPath.GetString() + "/" + finalName)
|
||||
{
|
||||
m_actualNewPath = dstParentPath.AppendChild(TfToken(finalName));
|
||||
}
|
||||
|
||||
static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) {
|
||||
if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath())
|
||||
return stage->GetPseudoRoot();
|
||||
return stage->GetPrimAtPath(parentPath);
|
||||
}
|
||||
|
||||
void ReparentPrimCommand::Execute() {
|
||||
UsdPrim src = m_stage->GetPrimAtPath(m_srcPath);
|
||||
if (!src.IsValid()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Execute: source prim not found: " + m_srcPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdPrim dst = GetParentPrim(m_stage, m_dstParentPath);
|
||||
if (!dst.IsValid()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Execute: destination parent not found: " + m_dstParentPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
editor.ReparentPrim(src, dst, TfToken(m_finalName));
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("ReparentPrimCommand::Execute: cannot reparent " + m_srcPath.GetString() + ": " + whyNot);
|
||||
return;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Execute: ApplyEdits failed");
|
||||
}
|
||||
}
|
||||
|
||||
void ReparentPrimCommand::Undo() {
|
||||
UsdPrim moved = m_stage->GetPrimAtPath(m_actualNewPath);
|
||||
if (!moved.IsValid()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Undo: moved prim not found: " + m_actualNewPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdPrim origParent = GetParentPrim(m_stage, m_origParentPath);
|
||||
if (!origParent.IsValid()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Undo: original parent not found: " + m_origParentPath.GetString());
|
||||
return;
|
||||
}
|
||||
UsdNamespaceEditor editor(m_stage);
|
||||
editor.ReparentPrim(moved, origParent, TfToken(m_origName));
|
||||
std::string whyNot;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("ReparentPrimCommand::Undo: cannot reparent back " + m_actualNewPath.GetString() + ": " + whyNot);
|
||||
return;
|
||||
}
|
||||
if (!editor.ApplyEdits()) {
|
||||
LOG_ERROR("ReparentPrimCommand::Undo: ApplyEdits failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Reparents a USD prim under a new parent via UsdNamespaceEditor.
|
||||
/// Supports undo by reparenting back to the original parent with the original name.
|
||||
/// Pass SdfPath("/") as dstParentPath to reparent to the stage root.
|
||||
class ReparentPrimCommand : public ICommand {
|
||||
public:
|
||||
ReparentPrimCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& srcPath,
|
||||
const pxr::SdfPath& dstParentPath,
|
||||
const std::string& finalName);
|
||||
|
||||
void Execute() override;
|
||||
void Undo() override;
|
||||
std::string GetDescription() const override { return m_description; }
|
||||
|
||||
private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_srcPath; ///< original path before reparent
|
||||
pxr::SdfPath m_origParentPath; ///< parent path before reparent
|
||||
std::string m_origName; ///< prim name before reparent
|
||||
pxr::SdfPath m_dstParentPath; ///< target parent path
|
||||
std::string m_finalName; ///< name inside new parent (collision-free)
|
||||
pxr::SdfPath m_actualNewPath; ///< full path after Execute (set in Execute)
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -1,9 +1,52 @@
|
||||
#include "TransformCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/editContext.h>
|
||||
#include <pxr/usd/usdGeom/xformOp.h>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
// True when attr has an authored time sample exactly at time t.
|
||||
static bool HasSampleAtTime(const pxr::UsdAttribute& attr, double t)
|
||||
{
|
||||
if (!attr) return false;
|
||||
std::vector<double> times;
|
||||
if (!attr.GetTimeSamples(×)) return false;
|
||||
for (double s : times)
|
||||
if (s == t) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attribute name of the rotate op XformCommonAPI authors for a rotation order
|
||||
// (e.g. "xformOp:rotateXYZ").
|
||||
static pxr::TfToken RotateOpName(pxr::UsdGeomXformCommonAPI::RotationOrder ro)
|
||||
{
|
||||
return pxr::UsdGeomXformOp::GetOpName(
|
||||
pxr::UsdGeomXformCommonAPI::ConvertRotationOrderToOpType(ro));
|
||||
}
|
||||
|
||||
TransformCommand::SamplePresence TransformCommand::QuerySamplePresence(
|
||||
pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& primPath,
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
|
||||
pxr::UsdTimeCode timeCode)
|
||||
{
|
||||
SamplePresence presence;
|
||||
if (timeCode.IsDefault() || !stage) return presence;
|
||||
pxr::UsdPrim prim = stage->GetPrimAtPath(primPath);
|
||||
if (!prim) return presence;
|
||||
|
||||
double t = timeCode.GetValue();
|
||||
presence.translate = HasSampleAtTime(
|
||||
prim.GetAttribute(pxr::UsdGeomXformOp::GetOpName(
|
||||
pxr::UsdGeomXformOp::TypeTranslate)), t);
|
||||
presence.rotate = HasSampleAtTime(
|
||||
prim.GetAttribute(RotateOpName(rotOrder)), t);
|
||||
presence.scale = HasSampleAtTime(
|
||||
prim.GetAttribute(pxr::UsdGeomXformOp::GetOpName(
|
||||
pxr::UsdGeomXformOp::TypeScale)), t);
|
||||
return presence;
|
||||
}
|
||||
|
||||
TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& primPath,
|
||||
pxr::SdfLayerHandle editLayer,
|
||||
@@ -14,6 +57,8 @@ TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::GfVec3f& newRotate,
|
||||
const pxr::GfVec3f& newScale,
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
|
||||
pxr::UsdTimeCode timeCode,
|
||||
SamplePresence preEditSamples,
|
||||
std::string description)
|
||||
: m_stage(stage)
|
||||
, m_primPath(primPath)
|
||||
@@ -25,7 +70,9 @@ TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage,
|
||||
, m_newRotate(newRotate)
|
||||
, m_newScale(newScale)
|
||||
, m_rotOrder(rotOrder)
|
||||
, m_timeCode(timeCode)
|
||||
, m_description(std::move(description))
|
||||
, m_preEditSamples(preEditSamples)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,6 +82,7 @@ void TransformCommand::Execute() {
|
||||
|
||||
void TransformCommand::Undo() {
|
||||
Apply(m_oldTranslate, m_oldRotate, m_oldScale);
|
||||
ClearCreatedSamples();
|
||||
}
|
||||
|
||||
void TransformCommand::Apply(const pxr::GfVec3d& t,
|
||||
@@ -51,17 +99,50 @@ void TransformCommand::Apply(const pxr::GfVec3d& t,
|
||||
try {
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
api.SetTranslate(t, pxr::UsdTimeCode::Default());
|
||||
api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default());
|
||||
api.SetScale(s, pxr::UsdTimeCode::Default());
|
||||
api.SetTranslate(t, m_timeCode);
|
||||
api.SetRotate(r, m_rotOrder, m_timeCode);
|
||||
api.SetScale(s, m_timeCode);
|
||||
} else {
|
||||
api.SetTranslate(t, pxr::UsdTimeCode::Default());
|
||||
api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default());
|
||||
api.SetScale(s, pxr::UsdTimeCode::Default());
|
||||
api.SetTranslate(t, m_timeCode);
|
||||
api.SetRotate(r, m_rotOrder, m_timeCode);
|
||||
api.SetScale(s, m_timeCode);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("TransformCommand::Apply error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void TransformCommand::ClearCreatedSamples()
|
||||
{
|
||||
if (m_timeCode.IsDefault()) return;
|
||||
if (!m_stage || m_primPath.IsEmpty()) return;
|
||||
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
|
||||
if (!prim) return;
|
||||
|
||||
auto clearOp = [&](const pxr::TfToken& opName, bool hadSample) {
|
||||
if (hadSample) return;
|
||||
pxr::UsdAttribute attr = prim.GetAttribute(opName);
|
||||
if (attr) attr.ClearAtTime(m_timeCode);
|
||||
};
|
||||
|
||||
auto clearAll = [&]() {
|
||||
clearOp(pxr::UsdGeomXformOp::GetOpName(
|
||||
pxr::UsdGeomXformOp::TypeTranslate), m_preEditSamples.translate);
|
||||
clearOp(RotateOpName(m_rotOrder), m_preEditSamples.rotate);
|
||||
clearOp(pxr::UsdGeomXformOp::GetOpName(
|
||||
pxr::UsdGeomXformOp::TypeScale), m_preEditSamples.scale);
|
||||
};
|
||||
|
||||
try {
|
||||
if (m_editLayer) {
|
||||
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||
clearAll();
|
||||
} else {
|
||||
clearAll();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(std::string("TransformCommand::ClearCreatedSamples error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/usd/timeCode.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/layer.h>
|
||||
#include <pxr/base/gf/vec3d.h>
|
||||
@@ -14,8 +15,26 @@ namespace UsdLayerManager {
|
||||
/// Reverses a complete TRS edit on a single prim (from gizmo drag or
|
||||
/// Property panel field commit). Stores the prim path, edit-target layer,
|
||||
/// and pre/post translate/rotate/scale triples.
|
||||
///
|
||||
/// When timeCode is not Default() the edit is a keyframe: values are written
|
||||
/// as time samples at that frame. Undo removes samples this command created
|
||||
/// on ops that had no sample at that frame before the edit.
|
||||
class TransformCommand : public ICommand {
|
||||
public:
|
||||
/// Whether each common op had an authored sample at the edit time.
|
||||
/// Must be queried BEFORE the first live write of an interactive edit
|
||||
/// (live writes author the sample, so querying later always finds one).
|
||||
struct SamplePresence {
|
||||
bool translate = false;
|
||||
bool rotate = false;
|
||||
bool scale = false;
|
||||
};
|
||||
static SamplePresence QuerySamplePresence(
|
||||
pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& primPath,
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
|
||||
pxr::UsdTimeCode timeCode);
|
||||
|
||||
TransformCommand(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath& primPath,
|
||||
pxr::SdfLayerHandle editLayer,
|
||||
@@ -26,6 +45,8 @@ public:
|
||||
const pxr::GfVec3f& newRotate,
|
||||
const pxr::GfVec3f& newScale,
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
|
||||
pxr::UsdTimeCode timeCode,
|
||||
SamplePresence preEditSamples,
|
||||
std::string description = "Transform");
|
||||
|
||||
void Execute() override;
|
||||
@@ -36,6 +57,9 @@ private:
|
||||
void Apply(const pxr::GfVec3d& t,
|
||||
const pxr::GfVec3f& r,
|
||||
const pxr::GfVec3f& s);
|
||||
/// Remove the time samples Execute created on ops that previously had
|
||||
/// no sample at m_timeCode (no-op when m_timeCode is Default).
|
||||
void ClearCreatedSamples();
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
pxr::SdfPath m_primPath;
|
||||
@@ -50,7 +74,12 @@ private:
|
||||
pxr::GfVec3f m_newScale;
|
||||
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder m_rotOrder;
|
||||
pxr::UsdTimeCode m_timeCode;
|
||||
std::string m_description;
|
||||
|
||||
// Whether each op already had a sample at m_timeCode before the edit
|
||||
// (only meaningful when m_timeCode is not Default).
|
||||
SamplePresence m_preEditSamples;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "ui/Application.h"
|
||||
#include "utils/Logger.h"
|
||||
#include <pxr/base/plug/registry.h>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
static void SetUsdPluginPath() {
|
||||
char exePath[MAX_PATH];
|
||||
@@ -17,6 +19,45 @@ static void SetUsdPluginPath() {
|
||||
std::string pluginPath = exeDir + "\\usd";
|
||||
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
|
||||
|
||||
// --- MaterialX / Arnold OSL include path -------------------------------
|
||||
// hdArnold compiles MaterialX shaders by generating OSL whose first line is
|
||||
// #include "mx_funcs.h"
|
||||
// and compiling it as an "osl" node's `code` parameter. Without an include
|
||||
// path that compile fails with:
|
||||
// [osl] error: <buffer>:2:10: fatal error: 'mx_funcs.h' file not found
|
||||
//
|
||||
// The knob is Arnold's options.osl_includepath, which hdArnold exposes as the
|
||||
// HDARNOLD_osl_includepath env setting (render_delegate/config.cpp).
|
||||
//
|
||||
// THIS MUST BE SET BEFORE ANY PLUGIN DLL IS LOADED. TF_DEFINE_ENV_SETTING
|
||||
// registers a TF_REGISTRY_FUNCTION that eagerly calls TfGetEnvSetting() and
|
||||
// caches the value permanently when hdArnold.dll loads -- which the
|
||||
// LoadLibraryA pre-flight below does. Setting it later (e.g. from
|
||||
// Application::Initialize) is silently ignored.
|
||||
//
|
||||
// Set via both the CRT (_putenv_s -> getenv) and the Win32 block
|
||||
// (SetEnvironmentVariableA) since the two are not kept in sync on Windows.
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
const std::string mtlxLibs = exeDir + "\\libraries";
|
||||
const std::string mtlxOslInclude = mtlxLibs + "\\stdlib\\genosl\\include";
|
||||
|
||||
if (fs::exists(mtlxOslInclude)) {
|
||||
if (!getenv("HDARNOLD_osl_includepath")) {
|
||||
_putenv_s("HDARNOLD_osl_includepath", mtlxOslInclude.c_str());
|
||||
SetEnvironmentVariableA("HDARNOLD_osl_includepath",
|
||||
mtlxOslInclude.c_str());
|
||||
}
|
||||
// Nodedef lookup (MATERIALX_NODE_DEFINITIONS). Separate knob from
|
||||
// the OSL include path above -- it does NOT fix the include error.
|
||||
if (!getenv("PXR_MTLX_STDLIB_SEARCH_PATHS")) {
|
||||
_putenv_s("PXR_MTLX_STDLIB_SEARCH_PATHS", mtlxLibs.c_str());
|
||||
SetEnvironmentVariableA("PXR_MTLX_STDLIB_SEARCH_PATHS",
|
||||
mtlxLibs.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> pluginPaths;
|
||||
WIN32_FIND_DATAA findData;
|
||||
std::string searchPattern = pluginPath + "\\*";
|
||||
@@ -37,18 +78,75 @@ static void SetUsdPluginPath() {
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
if (!pluginPaths.empty()) {
|
||||
// Pre-flight: attempt to load each plugin DLL before registering it.
|
||||
// This catches missing dependencies (e.g. sycl8.dll → Intel GPU drivers)
|
||||
// gracefully and shows our own warning rather than a cryptic TF_ERROR.
|
||||
std::vector<std::string> checkedPaths;
|
||||
for (const auto& infoPath : pluginPaths) {
|
||||
// Derive the DLL path from the plugInfo.json location:
|
||||
// <plugin>/resources/plugInfo.json → ../<plugin>.dll
|
||||
// e.g. usd/hdCycles/resources/plugInfo.json → usd/hdCycles.dll
|
||||
size_t resPos = infoPath.rfind("\\resources\\plugInfo.json");
|
||||
if (resPos != std::string::npos) {
|
||||
std::string pluginDir = infoPath.substr(0, resPos);
|
||||
size_t dirSlash = pluginDir.rfind('\\');
|
||||
std::string pluginName = (dirSlash != std::string::npos)
|
||||
? pluginDir.substr(dirSlash + 1) : pluginDir;
|
||||
std::string dllPath = pluginPath + "\\" + pluginName + ".dll";
|
||||
DWORD dllAttrs = GetFileAttributesA(dllPath.c_str());
|
||||
if (dllAttrs != INVALID_FILE_ATTRIBUTES) {
|
||||
// DLL file exists — verify it can actually be loaded.
|
||||
// No LOAD_WITH_ALTERED_SEARCH_PATH: keep the application
|
||||
// directory (exe dir, where embree4.dll etc. live) in the
|
||||
// DLL search path so transitive deps resolve correctly.
|
||||
HMODULE h = LoadLibraryA(dllPath.c_str());
|
||||
if (!h) {
|
||||
DWORD err = GetLastError();
|
||||
char msg[256] = {};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, msg, static_cast<DWORD>(sizeof(msg) - 1), nullptr);
|
||||
std::string errMsg(msg);
|
||||
while (!errMsg.empty() && (errMsg.back() == '\r' || errMsg.back() == '\n'))
|
||||
errMsg.pop_back();
|
||||
char hexErr[12] = {};
|
||||
sprintf_s(hexErr, "0x%08X", static_cast<unsigned>(err));
|
||||
LOG_WARNING("Skipping plugin " + pluginName
|
||||
+ " (DLL load failed " + hexErr + "): " + errMsg);
|
||||
continue; // Skip registering this plugin
|
||||
}
|
||||
// Intentionally do NOT FreeLibrary here. Releasing the handle
|
||||
// drops the refcount to 0 and unloads the DLL; when USD's plug
|
||||
// system later loads it again DllMain re-fires, re-registering
|
||||
// any TfEnvSettings and causing "duplicate definition" warnings.
|
||||
// Keeping the handle loaded means USD's LoadLibrary is a no-op
|
||||
// (refcount bump only, no DllMain call), preventing duplicates.
|
||||
// The process holds these handles for its entire lifetime anyway.
|
||||
}
|
||||
}
|
||||
checkedPaths.push_back(infoPath);
|
||||
}
|
||||
|
||||
if (!checkedPaths.empty()) {
|
||||
auto& registry = pxr::PlugRegistry::GetInstance();
|
||||
auto registered = registry.RegisterPlugins(pluginPaths);
|
||||
auto registered = registry.RegisterPlugins(checkedPaths);
|
||||
LOG_INFO("Registered " + std::to_string(registered.size()) + " USD plugins from " + pluginPath);
|
||||
} else {
|
||||
LOG_WARNING("No USD plugins found at " + pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int main(int /*argc*/, char* /*argv*/[]) {
|
||||
try {
|
||||
UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info);
|
||||
// Temporary: capture LOG_INFO to a file so the [NodeGraph]/[ed] debug
|
||||
// traces are readable after the fact (this is a WIN32-subsystem app,
|
||||
// so stdout isn't visible when launched normally). Remove once the
|
||||
// node-editor drag regression is diagnosed.
|
||||
if (const char* appData = getenv("APPDATA")) {
|
||||
std::filesystem::path logDir = std::filesystem::path(appData) / "UsdLayerManager";
|
||||
std::filesystem::create_directories(logDir);
|
||||
UsdLayerManager::Logger::Instance().SetLogFile((logDir / "debug.log").string());
|
||||
}
|
||||
|
||||
LOG_INFO("=== USD Layer Manager Starting ===");
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#include "Application.h"
|
||||
#include "Application.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include "../utils/FileDialog.h"
|
||||
#include "../utils/PathUtils.h"
|
||||
#include "../utils/OcioConfigParser.h"
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/usd/references.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include "../utils/MovieEncoder.h"
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
@@ -35,6 +40,14 @@ static std::string SanitizeUsdNameApp(const std::string& raw) {
|
||||
Application::Application()
|
||||
: m_showDemoWindow(false)
|
||||
, m_showStageInfo(true)
|
||||
, m_showStageEditor(true)
|
||||
, m_showRenderLayerPanel(true)
|
||||
, m_showSceneHierarchy(true)
|
||||
, m_showViewport(true)
|
||||
, m_showPropertyPanel(true)
|
||||
, m_showTimeline(true)
|
||||
, m_showCurveEditor(false)
|
||||
, m_showMaterialEditor(false)
|
||||
, m_running(false) {
|
||||
}
|
||||
|
||||
@@ -55,29 +68,76 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
// Create managers
|
||||
m_stageManager = std::make_unique<UsdStageManager>();
|
||||
m_layerManager = std::make_unique<LayerManager>();
|
||||
m_renderLayerManager = std::make_unique<RenderLayerManager>();
|
||||
m_renderLayerManager->SetLayerManager(m_layerManager.get());
|
||||
m_propertyManager = std::make_unique<PropertyManager>();
|
||||
m_layerPanel = std::make_unique<LayerPanel>();
|
||||
m_layerPanel->SetLayerManager(m_layerManager.get());
|
||||
m_layerPanel->SetCommandHistory(&m_commandHistory);
|
||||
m_propertyManager->SetCommandHistory(&m_commandHistory);
|
||||
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
|
||||
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
|
||||
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
|
||||
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
|
||||
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
|
||||
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
|
||||
m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||
m_renderLayerPanel = std::make_unique<RenderLayerPanel>();
|
||||
m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||
m_renderLayerPanel->SetCommandHistory(&m_commandHistory);
|
||||
m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get());
|
||||
m_viewportPanel = std::make_unique<ViewportPanel>();
|
||||
m_viewportPanel->SetCommandHistory(&m_commandHistory);
|
||||
m_propertyPanel = std::make_unique<PropertyPanel>();
|
||||
m_propertyPanel->SetPropertyManager(m_propertyManager.get());
|
||||
m_propertyPanel->SetCommandHistory(&m_commandHistory);
|
||||
|
||||
m_curveEditorPanel = std::make_unique<CurveEditorPanel>();
|
||||
m_curveEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||
|
||||
m_materialManager = std::make_unique<MaterialManager>();
|
||||
|
||||
m_materialEditorPanel = std::make_unique<MaterialEditorPanel>();
|
||||
m_materialEditorPanel->SetMaterialManager(m_materialManager.get());
|
||||
m_materialEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||
|
||||
m_timelinePanel = std::make_unique<TimelinePanel>();
|
||||
m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime,
|
||||
pxr::UsdTimeCode editTime) {
|
||||
m_viewportPanel->SetTimeCodes(displayTime, editTime);
|
||||
m_propertyPanel->SetTimeCodes(displayTime, editTime);
|
||||
m_curveEditorPanel->SetTimeCodes(displayTime, editTime);
|
||||
};
|
||||
|
||||
m_curveEditorPanel->OnScrub = [this](double frame) {
|
||||
m_timelinePanel->SetCurrentFrameExternal(frame);
|
||||
};
|
||||
m_timelinePanel->OnBrowseFolder = [this]() -> std::string {
|
||||
HWND hwnd = m_imguiContext ? m_imguiContext->GetWindowHandle() : nullptr;
|
||||
return FileDialog::BrowseFolder("Select Playblast Output Folder", hwnd);
|
||||
};
|
||||
|
||||
// Initialize IconManager — must happen after OpenGL context is ready (ImGui init above).
|
||||
m_iconManager = std::make_unique<IconManager>();
|
||||
m_iconManager->Initialize(ResourcePath("resources/icons"), 24);
|
||||
m_sceneHierarchyPanel->SetIconManager(m_iconManager.get());
|
||||
m_propertyPanel->SetIconManager(m_iconManager.get());
|
||||
m_viewportPanel->SetIconManager(m_iconManager.get());
|
||||
m_timelinePanel->SetIconManager(m_iconManager.get());
|
||||
m_stageEditorPanel->SetIconManager(m_iconManager.get());
|
||||
m_materialEditorPanel->SetIconManager(m_iconManager.get());
|
||||
m_renderLayerPanel->SetIconManager(m_iconManager.get());
|
||||
|
||||
m_sceneHierarchyPanel->SetOnPrimSelected(
|
||||
[this](const std::string& path) {
|
||||
m_viewportPanel->SetSelectedPrimPath(path);
|
||||
m_propertyPanel->SetSelectedPrimPath(path);
|
||||
m_curveEditorPanel->SetSelectedPrimPath(path);
|
||||
m_materialEditorPanel->SetTargetPrimPath(path);
|
||||
});
|
||||
|
||||
m_sceneHierarchyPanel->SetOnPrimsSelected(
|
||||
[this](const std::vector<std::string>& paths) {
|
||||
m_viewportPanel->SetSelectedPrimPaths(paths);
|
||||
});
|
||||
|
||||
m_sceneHierarchyPanel->SetOnStageMetadataChanged(
|
||||
@@ -85,24 +145,81 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
RefreshManagers();
|
||||
});
|
||||
|
||||
// Single click in viewport → sync hierarchy + property panel
|
||||
// Single click in viewport → sync hierarchy + property panel + curve editor
|
||||
m_viewportPanel->OnPrimPicked = [this](const std::string& path) {
|
||||
m_sceneHierarchyPanel->SetSelectedPath(path);
|
||||
m_propertyPanel->SetSelectedPrimPath(path);
|
||||
m_curveEditorPanel->SetSelectedPrimPath(path);
|
||||
m_materialEditorPanel->SetTargetPrimPath(path);
|
||||
};
|
||||
|
||||
// Rect drag in viewport → sync hierarchy + property panel (primary path)
|
||||
// Rect drag in viewport → sync hierarchy + property panel + curve editor (primary)
|
||||
m_viewportPanel->OnPrimsPickedRect = [this](const std::vector<std::string>& paths) {
|
||||
m_sceneHierarchyPanel->SetSelectedPaths(paths);
|
||||
m_propertyPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
|
||||
m_curveEditorPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
|
||||
m_materialEditorPanel->SetTargetPrimPath(paths.empty() ? "" : paths.front());
|
||||
};
|
||||
|
||||
// "Edit Material" in the Property Panel's Material Binding section →
|
||||
// open the Material Editor on the resolved material.
|
||||
m_propertyPanel->OnEditMaterialRequested = [this](const std::string& materialPath) {
|
||||
m_showMaterialEditor = true;
|
||||
m_materialEditorPanel->OpenOrCreateMaterial(materialPath);
|
||||
};
|
||||
|
||||
|
||||
if (!m_stageManager->CreateInMemoryStage()) {
|
||||
LOG_ERROR("Failed to create default in-memory stage");
|
||||
} else {
|
||||
RefreshManagers();
|
||||
}
|
||||
|
||||
// Set OCIO to bundled ACES 1.2 config if not already configured externally.
|
||||
// Must happen before the first Render() call so Hydra picks up the env var.
|
||||
if (!getenv("OCIO")) {
|
||||
namespace fs = std::filesystem;
|
||||
std::string ocioConfig = ResourcePath(
|
||||
"resources/OpenColorIO-Configs/aces_1.2/config.ocio");
|
||||
if (fs::exists(ocioConfig)) {
|
||||
_putenv_s("OCIO", ocioConfig.c_str());
|
||||
LOG_INFO("OCIO config: " + ocioConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: HDARNOLD_osl_includepath / PXR_MTLX_STDLIB_SEARCH_PATHS are set in
|
||||
// main.cpp, NOT here. They must be in the environment before the plugin
|
||||
// DLLs are loaded, because TF_DEFINE_ENV_SETTING caches the value when
|
||||
// hdArnold.dll registers its settings. Setting them at this point is too
|
||||
// late and is silently ignored.
|
||||
if (const char* oslInc = getenv("HDARNOLD_osl_includepath")) {
|
||||
LOG_INFO(std::string("Arnold OSL include path: ") + oslInc);
|
||||
} else {
|
||||
LOG_WARNING("HDARNOLD_osl_includepath not set — Arnold MaterialX "
|
||||
"shaders will fail to compile ('mx_funcs.h' not found)");
|
||||
}
|
||||
|
||||
// Load per-viewport settings and global preferences from AppData.
|
||||
if (const char* appData = getenv("APPDATA")) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path dir = fs::path(appData) / "UsdLayerManager";
|
||||
fs::create_directories(dir);
|
||||
m_viewportSettingsPath = (dir / "viewport_settings.ini").string();
|
||||
m_prefsPath = (dir / "preferences.ini").string();
|
||||
m_viewportPanel->LoadSettings(m_viewportSettingsPath);
|
||||
LoadPreferences();
|
||||
ValidateOcioPreferences();
|
||||
// Apply pref delegate only to tiles that have no saved delegate.
|
||||
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
|
||||
// Color correction is always global — apply to all tiles.
|
||||
m_viewportPanel->ApplyGlobalColorCorrection(
|
||||
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
|
||||
m_materialEditorPanel->SetColorCorrectionFromPrefs(
|
||||
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
|
||||
}
|
||||
|
||||
LOG_INFO("Application initialized successfully");
|
||||
return true;
|
||||
}
|
||||
@@ -120,11 +237,21 @@ void Application::Run() {
|
||||
}
|
||||
|
||||
void Application::Shutdown() {
|
||||
if (m_viewportPanel && !m_viewportSettingsPath.empty())
|
||||
m_viewportPanel->SaveSettings(m_viewportSettingsPath);
|
||||
if (!m_prefsPath.empty())
|
||||
SavePreferences();
|
||||
|
||||
m_viewportPanel.reset();
|
||||
m_sceneHierarchyPanel.reset();
|
||||
m_propertyPanel.reset();
|
||||
m_layerPanel.reset();
|
||||
m_stageEditorPanel.reset();
|
||||
m_renderLayerPanel.reset();
|
||||
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
|
||||
// destroyed while the GL context still exists, like m_viewportPanel.
|
||||
m_materialEditorPanel.reset();
|
||||
m_propertyManager.reset();
|
||||
m_renderLayerManager.reset();
|
||||
m_layerManager.reset();
|
||||
|
||||
if (m_iconManager) {
|
||||
@@ -149,24 +276,44 @@ void Application::RefreshManagers() {
|
||||
if (m_stageManager->HasStage()) {
|
||||
auto stage = m_stageManager->GetStage();
|
||||
m_layerManager->SetStage(stage);
|
||||
// After LayerManager (ActivateRenderLayer routes edit-target changes
|
||||
// through it) but before RestorePersistedActiveLayer needs it.
|
||||
m_renderLayerManager->SetStage(stage);
|
||||
m_propertyManager->SetStage(stage);
|
||||
m_sceneHierarchyPanel->SetStage(stage);
|
||||
m_renderLayerPanel->SetStage(stage);
|
||||
m_viewportPanel->SetStage(stage);
|
||||
m_viewportPanel->FrameScene();
|
||||
m_propertyPanel->SetStage(stage);
|
||||
m_timelinePanel->SetStage(stage);
|
||||
m_curveEditorPanel->SetStage(stage);
|
||||
m_materialManager->SetStage(stage);
|
||||
m_materialEditorPanel->SetStage(stage);
|
||||
// Re-applies whichever render layer was active last time this stage
|
||||
// was saved (mute state itself isn't serialized — see
|
||||
// RenderLayerManager::RestorePersistedActiveLayer).
|
||||
m_renderLayerManager->RestorePersistedActiveLayer();
|
||||
} else {
|
||||
m_layerManager->SetStage(nullptr);
|
||||
m_renderLayerManager->SetStage(nullptr);
|
||||
m_propertyManager->SetStage(nullptr);
|
||||
m_sceneHierarchyPanel->SetStage(nullptr);
|
||||
m_renderLayerPanel->SetStage(nullptr);
|
||||
m_viewportPanel->SetStage(nullptr);
|
||||
m_propertyPanel->SetStage(nullptr);
|
||||
m_timelinePanel->SetStage(nullptr);
|
||||
m_curveEditorPanel->SetStage(nullptr);
|
||||
m_materialManager->SetStage(nullptr);
|
||||
m_materialEditorPanel->SetStage(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::Update() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
m_timelinePanel->Update(io.DeltaTime);
|
||||
|
||||
// Process undo/redo hotkeys (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z).
|
||||
// Only fire when no ImGui text-input widget has keyboard focus.
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (!io.WantTextInput) {
|
||||
if (io.KeyCtrl && !io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z, false)) {
|
||||
m_commandHistory.Undo();
|
||||
@@ -181,6 +328,10 @@ void Application::Update() {
|
||||
void Application::RenderUI() {
|
||||
m_imguiContext->NewFrame();
|
||||
|
||||
// Status bar must be rendered before DockSpaceOverViewport so it
|
||||
// reserves space at the bottom before the dockspace claims the rest.
|
||||
RenderStatusBar();
|
||||
|
||||
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
|
||||
|
||||
if (m_showDemoWindow) {
|
||||
@@ -193,26 +344,444 @@ void Application::RenderUI() {
|
||||
RenderStageInfo();
|
||||
}
|
||||
|
||||
ImGui::Begin("Layer Panel", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
m_layerPanel->Render();
|
||||
ImGui::End();
|
||||
if (m_showStageEditor) {
|
||||
ImGui::Begin("Stage Editor", &m_showStageEditor, ImGuiWindowFlags_NoCollapse);
|
||||
m_stageEditorPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
m_viewportPanel->Render();
|
||||
if (m_showRenderLayerPanel) {
|
||||
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
|
||||
m_renderLayerPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showViewport) {
|
||||
m_viewportPanel->Render(&m_showViewport);
|
||||
}
|
||||
|
||||
// Scene Hierarchy is rendered AFTER the viewport so that viewport picks
|
||||
// (OnPrimPicked / OnPrimsPickedRect) are visible to the hierarchy in the
|
||||
// same frame — eliminating the one-frame-late scroll/highlight lag.
|
||||
ImGui::Begin("Scene Hierarchy", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
m_sceneHierarchyPanel->Render();
|
||||
ImGui::End();
|
||||
if (m_showSceneHierarchy) {
|
||||
ImGui::Begin("Scene Hierarchy", &m_showSceneHierarchy, ImGuiWindowFlags_NoCollapse);
|
||||
m_sceneHierarchyPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
ImGui::Begin("Property Panel", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
m_propertyPanel->Render();
|
||||
ImGui::End();
|
||||
if (m_showPropertyPanel) {
|
||||
ImGui::Begin("Property Panel", &m_showPropertyPanel, ImGuiWindowFlags_NoCollapse);
|
||||
m_propertyPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showTimeline) {
|
||||
ImGui::Begin("Timeline", &m_showTimeline, ImGuiWindowFlags_NoCollapse);
|
||||
m_timelinePanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showCurveEditor) {
|
||||
ImGui::SetNextWindowSize({960, 320}, ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Curve Editor", &m_showCurveEditor, ImGuiWindowFlags_NoCollapse);
|
||||
m_curveEditorPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showMaterialEditor) {
|
||||
ImGui::SetNextWindowSize({960, 320}, ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Material Editor", &m_showMaterialEditor, ImGuiWindowFlags_NoCollapse);
|
||||
m_materialEditorPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showPreferences)
|
||||
RenderPreferencesDialog();
|
||||
|
||||
// Capture happens after the timeline so that the "Start" button click in
|
||||
// this frame is detected, and after the viewport has rendered the scene.
|
||||
if (m_timelinePanel->IsPlayblasting()) {
|
||||
CapturePlayblastFrame();
|
||||
}
|
||||
|
||||
// If user aborted playblast mid-sequence, close any open encoder.
|
||||
if (m_movieEncoder.IsOpen() && !m_timelinePanel->IsPlayblasting()) {
|
||||
m_movieEncoder.Close();
|
||||
}
|
||||
|
||||
m_imguiContext->Render();
|
||||
}
|
||||
|
||||
void Application::RenderStatusBar() {
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float height = ImGui::GetFrameHeight();
|
||||
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_MenuBar;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 2.f));
|
||||
bool open = ImGui::BeginViewportSideBar("##statusbar", vp,
|
||||
ImGuiDir_Down, height, flags);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (open) {
|
||||
ImGui::BeginMenuBar();
|
||||
|
||||
// FPS — right-aligned
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "FPS: %.1f", io.Framerate);
|
||||
float textW = ImGui::CalcTextSize(buf).x;
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
if (avail > textW)
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - textW);
|
||||
ImGui::TextDisabled("%s", buf);
|
||||
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global preferences persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
void Application::LoadPreferences()
|
||||
{
|
||||
std::ifstream f(m_prefsPath);
|
||||
if (!f) return;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
if (line.empty() || line.front() == '#' || line.front() == '[') continue;
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string key = line.substr(0, eq);
|
||||
std::string val = line.substr(eq + 1);
|
||||
if (key == "RenderDelegate") m_prefs.renderDelegate = val;
|
||||
else if (key == "ColorCorrectionMode")
|
||||
m_prefs.colorCorrectionMode = [&]{ try { return std::stoi(val); } catch(...){ return 1; } }();
|
||||
else if (key == "OcioDisplay") m_prefs.ocioDisplay = val;
|
||||
else if (key == "OcioView") m_prefs.ocioView = val;
|
||||
else if (key == "OcioColorSpace") m_prefs.ocioColorSpace = val;
|
||||
else if (key == "OcioLook") m_prefs.ocioLook = val;
|
||||
else if (key == "MaterialBrowserWidth")
|
||||
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
|
||||
else if (key == "MaterialPreviewWidth")
|
||||
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }();
|
||||
else if (key == "KeepGraphNodeViewSettingsInUsd")
|
||||
m_prefs.keepGraphNodeViewSettingsInUsd = (val == "1");
|
||||
}
|
||||
|
||||
if (m_materialEditorPanel) {
|
||||
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
|
||||
m_prefs.materialPreviewWidth);
|
||||
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::ValidateOcioPreferences()
|
||||
{
|
||||
// A persisted display/view that doesn't exist in the active OCIO config
|
||||
// makes HdxColorCorrectionTask throw ("Display 'x' not found") every frame
|
||||
// and silently skip correction, which reads as a washed-out viewport --
|
||||
// worst on delegates like hdEmbree whose output depends on the transform.
|
||||
// Note display and view are easy to invert: in the bundled ACES 1.2 config
|
||||
// the only display is "ACES" and "sRGB" is one of its views.
|
||||
const OcioConfig& cfg = GetCurrentOcioConfig();
|
||||
if (!cfg.valid) {
|
||||
// No $OCIO / unreadable config -- nothing to validate against. Leave
|
||||
// the prefs alone rather than clobbering values that may be correct
|
||||
// for a config supplied later.
|
||||
return;
|
||||
}
|
||||
|
||||
auto contains = [](const std::vector<std::string>& v, const std::string& s) {
|
||||
return std::find(v.begin(), v.end(), s) != v.end();
|
||||
};
|
||||
|
||||
// --- display ---
|
||||
if (m_prefs.ocioDisplay.empty() || !contains(cfg.displays, m_prefs.ocioDisplay)) {
|
||||
if (!m_prefs.ocioDisplay.empty()) {
|
||||
LOG_WARNING("OCIO display '" + m_prefs.ocioDisplay
|
||||
+ "' not in config; falling back to '"
|
||||
+ cfg.defaultDisplay + "'");
|
||||
}
|
||||
m_prefs.ocioDisplay = cfg.defaultDisplay;
|
||||
}
|
||||
|
||||
// --- view (must belong to the display resolved above) ---
|
||||
auto viewsIt = cfg.views.find(m_prefs.ocioDisplay);
|
||||
const std::vector<std::string>* views =
|
||||
(viewsIt != cfg.views.end()) ? &viewsIt->second : nullptr;
|
||||
|
||||
if (views && !views->empty()) {
|
||||
if (m_prefs.ocioView.empty() || !contains(*views, m_prefs.ocioView)) {
|
||||
// Prefer the config default when it's valid for this display,
|
||||
// otherwise take the display's first view.
|
||||
const std::string fallback =
|
||||
contains(*views, cfg.defaultView) ? cfg.defaultView : views->front();
|
||||
if (!m_prefs.ocioView.empty()) {
|
||||
LOG_WARNING("OCIO view '" + m_prefs.ocioView
|
||||
+ "' not valid for display '" + m_prefs.ocioDisplay
|
||||
+ "'; falling back to '" + fallback + "'");
|
||||
}
|
||||
m_prefs.ocioView = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// --- color space (same failure mode if it names a missing space) ---
|
||||
if (!m_prefs.ocioColorSpace.empty()
|
||||
&& !contains(cfg.colorSpaces, m_prefs.ocioColorSpace)) {
|
||||
LOG_WARNING("OCIO color space '" + m_prefs.ocioColorSpace
|
||||
+ "' not in config; clearing it");
|
||||
m_prefs.ocioColorSpace.clear();
|
||||
}
|
||||
|
||||
// --- look ---
|
||||
if (!m_prefs.ocioLook.empty()
|
||||
&& !contains(cfg.looks, m_prefs.ocioLook)) {
|
||||
LOG_WARNING("OCIO look '" + m_prefs.ocioLook
|
||||
+ "' not in config; clearing it");
|
||||
m_prefs.ocioLook.clear();
|
||||
}
|
||||
|
||||
LOG_INFO("OCIO display/view: '" + m_prefs.ocioDisplay + "' / '"
|
||||
+ m_prefs.ocioView + "'");
|
||||
}
|
||||
|
||||
void Application::SavePreferences()
|
||||
{
|
||||
// Pull the latest splitter positions; called from Shutdown before the
|
||||
// panel is reset, and from the preferences dialog while it's alive.
|
||||
if (m_materialEditorPanel)
|
||||
m_materialEditorPanel->GetColumnWidths(m_prefs.materialBrowserWidth,
|
||||
m_prefs.materialPreviewWidth);
|
||||
|
||||
std::ofstream f(m_prefsPath);
|
||||
if (!f) return;
|
||||
f << "[Preferences]\n";
|
||||
f << "RenderDelegate=" << m_prefs.renderDelegate << "\n";
|
||||
f << "ColorCorrectionMode=" << m_prefs.colorCorrectionMode << "\n";
|
||||
f << "OcioDisplay=" << m_prefs.ocioDisplay << "\n";
|
||||
f << "OcioView=" << m_prefs.ocioView << "\n";
|
||||
f << "OcioColorSpace=" << m_prefs.ocioColorSpace << "\n";
|
||||
f << "OcioLook=" << m_prefs.ocioLook << "\n";
|
||||
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
|
||||
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
|
||||
f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n";
|
||||
}
|
||||
|
||||
void Application::ApplyPrefsToAllViewports()
|
||||
{
|
||||
if (!m_viewportPanel) return;
|
||||
m_viewportPanel->ApplyGlobalColorCorrection(
|
||||
m_prefs.colorCorrectionMode,
|
||||
m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView,
|
||||
m_prefs.ocioColorSpace,
|
||||
m_prefs.ocioLook);
|
||||
if (m_materialEditorPanel)
|
||||
m_materialEditorPanel->SetColorCorrectionFromPrefs(
|
||||
m_prefs.colorCorrectionMode,
|
||||
m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView,
|
||||
m_prefs.ocioColorSpace,
|
||||
m_prefs.ocioLook);
|
||||
}
|
||||
|
||||
void Application::RenderPreferencesDialog()
|
||||
{
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 400), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
|
||||
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||
if (!ImGui::Begin("Preferences", &m_showPreferences,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
bool ccChanged = false;
|
||||
|
||||
if (ImGui::BeginTabBar("PrefTabs")) {
|
||||
|
||||
// ── General ─────────────────────────────────────────────────────────
|
||||
if (ImGui::BeginTabItem("General")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("No general settings yet.");
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// ── Animation ───────────────────────────────────────────────────────
|
||||
if (ImGui::BeginTabItem("Animation")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("No animation settings yet.");
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// ── Material ────────────────────────────────────────────────────────
|
||||
if (ImGui::BeginTabItem("Material")) {
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Checkbox("Keep graph node view settings in USD customData",
|
||||
&m_prefs.keepGraphNodeViewSettingsInUsd)) {
|
||||
if (m_materialEditorPanel)
|
||||
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(
|
||||
m_prefs.keepGraphNodeViewSettingsInUsd);
|
||||
}
|
||||
ImGui::TextWrapped(
|
||||
"When enabled, each node's pin display mode (all pins vs. "
|
||||
"connected only) and any manually revealed pins are saved into "
|
||||
"the material file and restored when it's reopened. When "
|
||||
"disabled, this is a session-only view setting.");
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// ── Viewport ────────────────────────────────────────────────────────
|
||||
if (ImGui::BeginTabItem("Viewport")) {
|
||||
ImGui::Spacing();
|
||||
|
||||
// Render Delegate default — applied to viewports with no saved delegate on startup.
|
||||
// Each viewport's toolbar controls its delegate independently after load.
|
||||
ImGui::SeparatorText("Default Render Delegate");
|
||||
ImGui::TextDisabled("Applied to viewports with no saved setting on startup.");
|
||||
ImGui::Spacing();
|
||||
auto plugins = UsdSceneRenderer::GetRendererPlugins();
|
||||
pxr::TfToken curId(m_prefs.renderDelegate);
|
||||
for (const auto& pluginId : plugins) {
|
||||
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
|
||||
if (name.empty()) name = pluginId.GetString();
|
||||
bool sel = (pluginId == curId);
|
||||
if (ImGui::RadioButton(name.c_str(), sel))
|
||||
m_prefs.renderDelegate = pluginId.GetString();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Color Correction
|
||||
ImGui::SeparatorText("Color Correction");
|
||||
|
||||
auto ccItem = [&](const char* label, int mode) {
|
||||
if (ImGui::RadioButton(label, m_prefs.colorCorrectionMode == mode)) {
|
||||
m_prefs.colorCorrectionMode = mode;
|
||||
m_prefOcioFieldsSynced = false;
|
||||
ccChanged = true;
|
||||
}
|
||||
};
|
||||
ccItem("Disabled", 0);
|
||||
ccItem("sRGB", 1);
|
||||
ccItem("OpenColorIO", 2);
|
||||
|
||||
if (m_prefs.colorCorrectionMode == 2) {
|
||||
ImGui::Spacing();
|
||||
const OcioConfig& ocfg = GetCurrentOcioConfig();
|
||||
|
||||
if (!m_prefOcioFieldsSynced) {
|
||||
std::string disp = m_prefs.ocioDisplay.empty() ? ocfg.defaultDisplay : m_prefs.ocioDisplay;
|
||||
std::string view = m_prefs.ocioView.empty() ? ocfg.defaultView : m_prefs.ocioView;
|
||||
strncpy(m_prefOcioDisplayBuf, disp.c_str(), 127);
|
||||
strncpy(m_prefOcioViewBuf, view.c_str(), 127);
|
||||
strncpy(m_prefOcioColorSpaceBuf, m_prefs.ocioColorSpace.c_str(), 127);
|
||||
strncpy(m_prefOcioLookBuf, m_prefs.ocioLook.c_str(), 127);
|
||||
if (m_prefs.ocioDisplay.empty()) m_prefs.ocioDisplay = disp;
|
||||
if (m_prefs.ocioView.empty()) m_prefs.ocioView = view;
|
||||
m_prefOcioFieldsSynced = true;
|
||||
}
|
||||
|
||||
ImGui::PushItemWidth(280.f);
|
||||
|
||||
if (ImGui::BeginCombo("Display##pref", m_prefOcioDisplayBuf)) {
|
||||
for (const auto& d : ocfg.displays) {
|
||||
bool sel = (d == m_prefOcioDisplayBuf);
|
||||
if (ImGui::Selectable(d.c_str(), sel)) {
|
||||
strncpy(m_prefOcioDisplayBuf, d.c_str(), 127);
|
||||
m_prefs.ocioDisplay = d;
|
||||
auto vit = ocfg.views.find(d);
|
||||
if (vit != ocfg.views.end() && !vit->second.empty()) {
|
||||
strncpy(m_prefOcioViewBuf, vit->second[0].c_str(), 127);
|
||||
m_prefs.ocioView = vit->second[0];
|
||||
}
|
||||
ccChanged = true;
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
{
|
||||
static const std::vector<std::string> kEmpty;
|
||||
auto vit = ocfg.views.find(std::string(m_prefOcioDisplayBuf));
|
||||
const auto& views = (vit != ocfg.views.end()) ? vit->second : kEmpty;
|
||||
if (ImGui::BeginCombo("View##pref", m_prefOcioViewBuf)) {
|
||||
for (const auto& v : views) {
|
||||
bool sel = (v == m_prefOcioViewBuf);
|
||||
if (ImGui::Selectable(v.c_str(), sel)) {
|
||||
strncpy(m_prefOcioViewBuf, v.c_str(), 127);
|
||||
m_prefs.ocioView = v;
|
||||
ccChanged = true;
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo("Color Space##pref",
|
||||
m_prefOcioColorSpaceBuf[0] ? m_prefOcioColorSpaceBuf : "(default)")) {
|
||||
if (ImGui::Selectable("(default)", m_prefOcioColorSpaceBuf[0] == '\0')) {
|
||||
m_prefOcioColorSpaceBuf[0] = '\0';
|
||||
m_prefs.ocioColorSpace.clear();
|
||||
ccChanged = true;
|
||||
}
|
||||
if (m_prefOcioColorSpaceBuf[0] == '\0') ImGui::SetItemDefaultFocus();
|
||||
for (const auto& cs : ocfg.colorSpaces) {
|
||||
bool sel = (cs == m_prefOcioColorSpaceBuf);
|
||||
if (ImGui::Selectable(cs.c_str(), sel)) {
|
||||
strncpy(m_prefOcioColorSpaceBuf, cs.c_str(), 127);
|
||||
m_prefs.ocioColorSpace = cs;
|
||||
ccChanged = true;
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo("Look##pref",
|
||||
m_prefOcioLookBuf[0] ? m_prefOcioLookBuf : "(none)")) {
|
||||
if (ImGui::Selectable("(none)", m_prefOcioLookBuf[0] == '\0')) {
|
||||
m_prefOcioLookBuf[0] = '\0';
|
||||
m_prefs.ocioLook.clear();
|
||||
ccChanged = true;
|
||||
}
|
||||
if (m_prefOcioLookBuf[0] == '\0') ImGui::SetItemDefaultFocus();
|
||||
for (const auto& look : ocfg.looks) {
|
||||
bool sel = (look == m_prefOcioLookBuf);
|
||||
if (ImGui::Selectable(look.c_str(), sel)) {
|
||||
strncpy(m_prefOcioLookBuf, look.c_str(), 127);
|
||||
m_prefs.ocioLook = look;
|
||||
ccChanged = true;
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
if (ccChanged)
|
||||
ApplyPrefsToAllViewports();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void Application::RenderMenuBar() {
|
||||
if (ImGui::BeginMainMenuBar()) {
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
@@ -270,6 +839,10 @@ void Application::RenderMenuBar() {
|
||||
m_commandHistory.Redo();
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Preferences..."))
|
||||
m_showPreferences = true;
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
@@ -302,8 +875,17 @@ void Application::RenderMenuBar() {
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo);
|
||||
ImGui::MenuItem("Show Demo Window", nullptr, &m_showDemoWindow);
|
||||
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
|
||||
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
|
||||
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
|
||||
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
|
||||
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
|
||||
ImGui::MenuItem("Timeline", nullptr, &m_showTimeline);
|
||||
ImGui::MenuItem("Curve Editor", nullptr, &m_showCurveEditor);
|
||||
ImGui::MenuItem("Material Editor", nullptr, &m_showMaterialEditor);
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo);
|
||||
ImGui::MenuItem("Demo Window", nullptr, &m_showDemoWindow);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -380,9 +962,19 @@ void Application::SaveUsdFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current stage is in-memory (anonymous root layer), fall through to
|
||||
// Save As — UsdStage::Save() cannot write anonymous layers to disk.
|
||||
auto rootLayer = m_stageManager->GetRootLayer();
|
||||
if (!rootLayer || rootLayer->IsAnonymous()) {
|
||||
SaveUsdFileAs();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_stageManager->SaveStage()) {
|
||||
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
||||
return;
|
||||
}
|
||||
SaveDirtyRenderLayers();
|
||||
}
|
||||
|
||||
void Application::SaveUsdFileAs() {
|
||||
@@ -400,10 +992,30 @@ void Application::SaveUsdFileAs() {
|
||||
if (!filePath.empty()) {
|
||||
if (!m_stageManager->SaveStageAs(filePath)) {
|
||||
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
||||
} else {
|
||||
// m_renderLayerManager's layer refs are identifier-keyed SdfLayer
|
||||
// objects, independent of which UsdStage currently references
|
||||
// them — safe to save before the stage swap RefreshManagers()
|
||||
// below performs.
|
||||
SaveDirtyRenderLayers();
|
||||
// SaveStageAs reopens m_stageManager's stage from the new file path.
|
||||
// RefreshManagers syncs m_layerManager (and others) to that new stage;
|
||||
// without this, subsequent sublayer edits go to the old (now stale) stage
|
||||
// and are silently lost on the next save.
|
||||
RefreshManagers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::SaveDirtyRenderLayers() {
|
||||
if (!m_renderLayerManager) return;
|
||||
for (const auto& info : m_renderLayerManager->GetRenderLayers()) {
|
||||
if (info.isDefault || !info.layer) continue;
|
||||
if (info.layer->IsDirty())
|
||||
info.layer->Save();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::CloseUsdFile() {
|
||||
m_stageManager->CloseStage();
|
||||
// Re-create a fresh default stage so the app is always in an editable state.
|
||||
@@ -458,6 +1070,123 @@ void Application::AddReferenceToStage() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Writes top-down RGBA8 pixel data as a 24-bit BMP file.
|
||||
// Negative height in the DIB header tells viewers the rows are top-down.
|
||||
static bool WriteBMP(const char* path, int w, int h, const uint8_t* rgba) {
|
||||
FILE* f = nullptr;
|
||||
if (fopen_s(&f, path, "wb") != 0 || !f) return false;
|
||||
|
||||
int rowStride = (w * 3 + 3) & ~3;
|
||||
int dataSize = rowStride * h;
|
||||
int fileSize = 54 + dataSize;
|
||||
|
||||
uint8_t hdr[54] = {};
|
||||
hdr[0] = 'B'; hdr[1] = 'M';
|
||||
auto put32 = [&](int off, uint32_t v) { memcpy(hdr + off, &v, 4); };
|
||||
auto put32s = [&](int off, int32_t v) { memcpy(hdr + off, &v, 4); };
|
||||
auto put16 = [&](int off, uint16_t v) { memcpy(hdr + off, &v, 2); };
|
||||
put32(2, uint32_t(fileSize));
|
||||
put32(10, 54u);
|
||||
put32(14, 40u);
|
||||
put32s(18, w);
|
||||
put32s(22, -h); // negative = top-down storage
|
||||
put16(26, 1u);
|
||||
put16(28, 24u);
|
||||
put32(34, uint32_t(dataSize));
|
||||
fwrite(hdr, 1, 54, f);
|
||||
|
||||
std::vector<uint8_t> row(rowStride, 0);
|
||||
for (int y = 0; y < h; ++y) {
|
||||
const uint8_t* src = rgba + y * w * 4;
|
||||
for (int x = 0; x < w; ++x) {
|
||||
row[x*3+0] = src[x*4+2]; // B
|
||||
row[x*3+1] = src[x*4+1]; // G
|
||||
row[x*3+2] = src[x*4+0]; // R
|
||||
}
|
||||
fwrite(row.data(), 1, rowStride, f);
|
||||
}
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::CapturePlayblastFrame() {
|
||||
// The frame BeginPlayblast() is called, the viewport has already rendered
|
||||
// at the old time. Skip one frame so the next viewport render picks up the
|
||||
// start timecode before we read pixels.
|
||||
if (m_timelinePanel->GetPlayblastSkipFirst()) {
|
||||
m_timelinePanel->ClearPlayblastSkipFirst();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& settings = m_timelinePanel->GetPlayblastSettings();
|
||||
auto& renderer = m_viewportPanel->GetRenderer();
|
||||
|
||||
// Re-render at the requested capture resolution. The renderer retains the
|
||||
// camera matrices from the viewport render, and SetFraming() adapts the
|
||||
// frustum to the new aspect ratio, so the output is correct regardless of
|
||||
// whether it matches the display viewport size.
|
||||
int cw = settings.captureWidth;
|
||||
int ch = settings.captureHeight;
|
||||
renderer.Render(cw, ch);
|
||||
|
||||
std::vector<uint8_t> pixels;
|
||||
if (!renderer.CaptureFrame(pixels)) {
|
||||
LOG_ERROR("Playblast: CaptureFrame failed");
|
||||
m_timelinePanel->AbortPlayblast();
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = m_timelinePanel->GetPlayblastFrameIndex();
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(settings.outputDir, ec);
|
||||
|
||||
// ── Movie encode (direct libav streaming) ─────────────────────────────
|
||||
if (settings.exportMovie) {
|
||||
if (idx == 0) {
|
||||
char moviePath[1024];
|
||||
snprintf(moviePath, sizeof(moviePath), "%s\\%s.mp4",
|
||||
settings.outputDir, settings.filePrefix);
|
||||
if (!m_movieEncoder.Open(moviePath, cw, ch, settings.fps)) {
|
||||
LOG_ERROR("MovieEncoder open failed: " + m_movieEncoder.GetLastError());
|
||||
m_timelinePanel->AbortPlayblast();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!m_movieEncoder.WriteFrame(pixels.data())) {
|
||||
LOG_ERROR("MovieEncoder write failed: " + m_movieEncoder.GetLastError());
|
||||
m_movieEncoder.Close();
|
||||
m_timelinePanel->AbortPlayblast();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── BMP frames (always written when not exporting movie, or when keepFrames) ─
|
||||
if (!settings.exportMovie || settings.keepFrames) {
|
||||
char filename[1024];
|
||||
snprintf(filename, sizeof(filename), "%s\\%s.%04d.bmp",
|
||||
settings.outputDir, settings.filePrefix, idx);
|
||||
if (!WriteBMP(filename, cw, ch, pixels.data())) {
|
||||
LOG_ERROR(std::string("Playblast: failed to write ") + filename);
|
||||
if (settings.exportMovie) m_movieEncoder.Close();
|
||||
m_timelinePanel->AbortPlayblast();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_timelinePanel->AdvancePlayblast()) {
|
||||
// Last frame — close the encoder (writes trailer).
|
||||
if (settings.exportMovie && m_movieEncoder.IsOpen()) {
|
||||
m_movieEncoder.Close();
|
||||
LOG_INFO(std::string("Playblast movie: ") +
|
||||
settings.outputDir + "\\" + settings.filePrefix + ".mp4");
|
||||
}
|
||||
LOG_INFO("Playblast complete: " + std::to_string(idx + 1) +
|
||||
" frames captured to " + settings.outputDir);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::CreatePrimOnStage(const std::string& typeName) {
|
||||
if (!m_stageManager->HasStage()) return;
|
||||
|
||||
|
||||
@@ -2,19 +2,43 @@
|
||||
|
||||
#include "ImGuiContext.h"
|
||||
#include "IconManager.h"
|
||||
#include "LayerPanel.h"
|
||||
#include "StageEditorPanel.h"
|
||||
#include "SceneHierarchyPanel.h"
|
||||
#include "RenderLayerPanel.h"
|
||||
#include "ViewportPanel.h"
|
||||
#include "PropertyPanel.h"
|
||||
#include "TimelinePanel.h"
|
||||
#include "CurveEditorPanel.h"
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "../core/UsdStageManager.h"
|
||||
#include "../core/LayerManager.h"
|
||||
#include "../core/RenderLayerManager.h"
|
||||
#include "../core/PropertyManager.h"
|
||||
#include "../core/MaterialManager.h"
|
||||
#include "../core/CommandHistory.h"
|
||||
#include "../utils/MovieEncoder.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Global (non-per-viewport) rendering preferences persisted to preferences.ini.
|
||||
struct AppPreferences {
|
||||
std::string renderDelegate = "HdStormRendererPlugin";
|
||||
int colorCorrectionMode = 1; ///< ColorCorrectionMode cast to int (1 = sRGB)
|
||||
std::string ocioDisplay;
|
||||
std::string ocioView;
|
||||
std::string ocioColorSpace;
|
||||
std::string ocioLook;
|
||||
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
|
||||
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
|
||||
/// When true, per-node pin display mode (Hypershade-style "show all" vs
|
||||
/// "show connected only") and revealed hidden pins are authored into USD
|
||||
/// customData so they survive reopening the material; when false, that
|
||||
/// state is session-only.
|
||||
bool keepGraphNodeViewSettingsInUsd = false;
|
||||
};
|
||||
|
||||
class Application {
|
||||
public:
|
||||
Application();
|
||||
@@ -28,33 +52,80 @@ private:
|
||||
void Update();
|
||||
void RenderUI();
|
||||
void RenderMenuBar();
|
||||
void RenderStatusBar();
|
||||
void RenderStageInfo();
|
||||
void RefreshManagers();
|
||||
|
||||
// File operations
|
||||
void OpenUsdFile();
|
||||
void CreateNewUsdFile(); // creates fresh in-memory stage
|
||||
void CreateNewUsdFile();
|
||||
void SaveUsdFile();
|
||||
void SaveUsdFileAs();
|
||||
void CloseUsdFile(); // closes file-backed stage, falls back to default stage
|
||||
void CloseUsdFile();
|
||||
|
||||
// Stage editing operations (also exposed via Stage menu)
|
||||
// Stage editing operations
|
||||
void AddReferenceToStage();
|
||||
void CreatePrimOnStage(const std::string& typeName);
|
||||
|
||||
/// Explicitly saves every render-layer SdfLayer, since at most one is
|
||||
/// ever in the stage's composition (unmuted) at a time and
|
||||
/// UsdStage::Save() only walks layers currently in composition — the
|
||||
/// rest would otherwise be silently dropped on save.
|
||||
void SaveDirtyRenderLayers();
|
||||
|
||||
// Playblast
|
||||
void CapturePlayblastFrame();
|
||||
|
||||
// Preferences
|
||||
void LoadPreferences();
|
||||
void SavePreferences();
|
||||
/// Reconcile persisted OCIO display/view names against the active config.
|
||||
/// Falls back to the config defaults when a name doesn't exist, so a stale
|
||||
/// or mistyped preference can't silently disable color correction.
|
||||
void ValidateOcioPreferences();
|
||||
void ApplyPrefsToAllViewports();
|
||||
void RenderPreferencesDialog();
|
||||
|
||||
std::unique_ptr<ImGuiContext> m_imguiContext;
|
||||
std::unique_ptr<IconManager> m_iconManager;
|
||||
std::unique_ptr<UsdStageManager> m_stageManager;
|
||||
std::unique_ptr<LayerManager> m_layerManager;
|
||||
std::unique_ptr<RenderLayerManager> m_renderLayerManager;
|
||||
std::unique_ptr<PropertyManager> m_propertyManager;
|
||||
std::unique_ptr<MaterialManager> m_materialManager;
|
||||
CommandHistory m_commandHistory;
|
||||
std::unique_ptr<LayerPanel> m_layerPanel;
|
||||
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
|
||||
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
|
||||
std::unique_ptr<RenderLayerPanel> m_renderLayerPanel;
|
||||
std::unique_ptr<ViewportPanel> m_viewportPanel;
|
||||
std::unique_ptr<PropertyPanel> m_propertyPanel;
|
||||
std::unique_ptr<TimelinePanel> m_timelinePanel;
|
||||
std::unique_ptr<CurveEditorPanel> m_curveEditorPanel;
|
||||
std::unique_ptr<MaterialEditorPanel> m_materialEditorPanel;
|
||||
bool m_showDemoWindow;
|
||||
bool m_showStageInfo;
|
||||
bool m_showStageEditor;
|
||||
bool m_showRenderLayerPanel;
|
||||
bool m_showSceneHierarchy;
|
||||
bool m_showViewport;
|
||||
bool m_showPropertyPanel;
|
||||
bool m_showTimeline;
|
||||
bool m_showCurveEditor;
|
||||
bool m_showMaterialEditor;
|
||||
bool m_running;
|
||||
|
||||
MovieEncoder m_movieEncoder;
|
||||
std::string m_viewportSettingsPath;
|
||||
std::string m_prefsPath;
|
||||
AppPreferences m_prefs;
|
||||
bool m_showPreferences = false;
|
||||
|
||||
// OCIO edit buffers for Preferences dialog
|
||||
char m_prefOcioDisplayBuf[128] = {};
|
||||
char m_prefOcioViewBuf[128] = {};
|
||||
char m_prefOcioColorSpaceBuf[128] = {};
|
||||
char m_prefOcioLookBuf[128] = {};
|
||||
bool m_prefOcioFieldsSynced = false;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/usd/attribute.h>
|
||||
#include <pxr/usd/usd/timeCode.h>
|
||||
#include <pxr/usd/usd/notice.h>
|
||||
#include <pxr/base/tf/weakBase.h>
|
||||
#include <pxr/base/tf/notice.h>
|
||||
#include <imgui.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
class CurveEditorPanel : public pxr::TfWeakBase {
|
||||
public:
|
||||
void SetStage(pxr::UsdStageRefPtr stage);
|
||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||
void SetSelectedPrimPath(const std::string& path);
|
||||
void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime);
|
||||
void Render();
|
||||
|
||||
// Fires when the user drags the time cursor; drives the Timeline panel.
|
||||
std::function<void(double frame)> OnScrub;
|
||||
|
||||
private:
|
||||
// ── Internal data ────────────────────────────────────────────────────────
|
||||
|
||||
struct BezierKey {
|
||||
double time = 0.0;
|
||||
double value = 0.0;
|
||||
// Tangent handles as (dt, dv) offsets from the key.
|
||||
// in-handle is left of the key (dt < 0), out-handle is right (dt > 0).
|
||||
double inTangentDt = -1.0, inTangentDv = 0.0;
|
||||
double outTangentDt = 1.0, outTangentDv = 0.0;
|
||||
bool tangentsBroken = false;
|
||||
};
|
||||
|
||||
struct CurveChannel {
|
||||
std::string displayName;
|
||||
pxr::UsdAttribute attr;
|
||||
int component = -1; // -1=scalar, 0/1/2/3 = X/Y/Z/W
|
||||
ImVec4 color = {1,1,1,1};
|
||||
bool visible = true;
|
||||
bool dirty = false;
|
||||
std::vector<double> origTimes;
|
||||
std::vector<double> origValues;
|
||||
std::vector<BezierKey> keys;
|
||||
};
|
||||
|
||||
enum class HandlePart { Key, InTangent, OutTangent };
|
||||
struct SelectedHandle { int chanIdx; int keyIdx; HandlePart part; };
|
||||
|
||||
// ── Methods ──────────────────────────────────────────────────────────────
|
||||
|
||||
void RefreshFromStage();
|
||||
void FitCurveFromSamples(CurveChannel& ch);
|
||||
void BakeChannelToUsd(CurveChannel& ch);
|
||||
|
||||
// Bezier math
|
||||
double EvaluateBezier(const std::vector<BezierKey>& keys, double T) const;
|
||||
void DrawBezierSegment(ImDrawList* dl, const BezierKey& k0, const BezierKey& k1,
|
||||
ImVec4 color, ImVec2 canvasMin, ImVec2 canvasMax) const;
|
||||
|
||||
// Coordinate helpers
|
||||
float TimeToX(double t, ImVec2 cMin, ImVec2 cMax) const;
|
||||
float ValToY (double v, ImVec2 cMin, ImVec2 cMax) const;
|
||||
double XToTime(float x, ImVec2 cMin, ImVec2 cMax) const;
|
||||
double YToVal (float y, ImVec2 cMin, ImVec2 cMax) const;
|
||||
ImVec2 KeyToScreen(const BezierKey& k, ImVec2 cMin, ImVec2 cMax) const;
|
||||
|
||||
// USD channel read/write helpers
|
||||
double ReadChannelValue (const pxr::UsdAttribute& attr, int comp, double time) const;
|
||||
void WriteChannelValue(const pxr::UsdAttribute& attr, int comp,
|
||||
double time, double val) const;
|
||||
void ClearAllTimeSamples(const pxr::UsdAttribute& attr) const;
|
||||
|
||||
// Rendering sub-sections
|
||||
void RenderToolbar();
|
||||
void RenderAttrList(float listWidth);
|
||||
void RenderCanvas(ImVec2 canvasMin, ImVec2 canvasMax);
|
||||
void RenderGrid(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
|
||||
void RenderTimeCursor(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
|
||||
void RenderCurve(ImDrawList* dl, CurveChannel& ch, ImVec2 cMin, ImVec2 cMax);
|
||||
void HandleCanvasInput(ImVec2 cMin, ImVec2 cMax);
|
||||
|
||||
// Selection helpers
|
||||
bool IsSelected(int chanIdx, int keyIdx, HandlePart part) const;
|
||||
void Select(int chanIdx, int keyIdx, HandlePart part, bool additive);
|
||||
void ClearSelection();
|
||||
|
||||
// Interaction helpers
|
||||
void FrameAll();
|
||||
void ApplyAllDirty();
|
||||
void RevertAll();
|
||||
void DeleteSelectedKeys();
|
||||
void AddKeyAt(int chanIdx, double time, double value);
|
||||
void RefitNeighborTangents(CurveChannel& ch, int keyIdx);
|
||||
|
||||
// Bezier metadata persistence (stored in prim customData["curveEditor"])
|
||||
static std::string ChannelMetadataKey(const CurveChannel& ch);
|
||||
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const CurveChannel& ch) const;
|
||||
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const std::string& channelKey,
|
||||
const std::vector<BezierKey>& keys) const;
|
||||
bool LoadBezierFromMetadata(const pxr::UsdPrim& prim, CurveChannel& ch) const;
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────────────
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
CommandHistory* m_commandHistory = nullptr;
|
||||
std::string m_selectedPrimPath;
|
||||
pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default();
|
||||
pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default();
|
||||
|
||||
std::vector<CurveChannel> m_channels;
|
||||
std::vector<SelectedHandle> m_selection;
|
||||
int m_activeChannel = -1; // for double-click-to-add
|
||||
|
||||
bool m_needsRefresh = true;
|
||||
bool m_suppressNotice = false; // set during bake writes to silence OnObjectsChanged
|
||||
|
||||
// USD change notification
|
||||
pxr::TfNotice::Key m_stageChangeKey;
|
||||
void OnObjectsChanged(const pxr::UsdNotice::ObjectsChanged& notice,
|
||||
const pxr::UsdStageWeakPtr& sender);
|
||||
|
||||
// View range
|
||||
double m_viewMinTime = 0.0, m_viewMaxTime = 100.0;
|
||||
double m_viewMinVal = -1.0, m_viewMaxVal = 1.0;
|
||||
|
||||
// Pan/zoom drag state
|
||||
bool m_isPanning = false;
|
||||
ImVec2 m_panLastMouse = {0,0};
|
||||
double m_panMinTimeStart = 0.0, m_panMaxTimeStart = 0.0;
|
||||
double m_panMinValStart = 0.0, m_panMaxValStart = 0.0;
|
||||
|
||||
// Drag-move state
|
||||
bool m_isDragging = false;
|
||||
ImVec2 m_dragStartMouse= {0,0};
|
||||
double m_dragStartTime = 0.0, m_dragStartValue = 0.0;
|
||||
struct KeySnapshot { int chanIdx; int keyIdx; double time; double value;
|
||||
double inDt, inDv, outDt, outDv; };
|
||||
std::vector<KeySnapshot> m_dragSnapshot;
|
||||
|
||||
// Selection box drag
|
||||
bool m_isBoxSelecting = false;
|
||||
ImVec2 m_boxSelectStart = {0,0};
|
||||
|
||||
// Scrub cursor drag
|
||||
bool m_isDraggingCursor = false;
|
||||
|
||||
// Options
|
||||
bool m_simplifyOnBake = false;
|
||||
float m_simplifyTol = 0.01f;
|
||||
bool m_snapToFrame = true;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -48,6 +48,21 @@ static const char* IconFilename(Icon icon) {
|
||||
case Icon::LayoutHSplit: return "layout-hsplit.svg";
|
||||
case Icon::LayoutVSplit: return "layout-vsplit.svg";
|
||||
case Icon::LayoutQuad: return "layout-quad.svg";
|
||||
case Icon::SkipBack: return "skip-back.svg";
|
||||
case Icon::StepBack: return "step-back.svg";
|
||||
case Icon::PlayBack: return "play-back.svg";
|
||||
case Icon::Play: return "play.svg";
|
||||
case Icon::Pause: return "pause.svg";
|
||||
case Icon::StepForward: return "step-forward.svg";
|
||||
case Icon::SkipEnd: return "skip-end.svg";
|
||||
case Icon::Loop: return "loop.svg";
|
||||
case Icon::Bounce: return "bounce.svg";
|
||||
case Icon::Refresh: return "arrows-rotate.svg";
|
||||
case Icon::FilePlus: return "file-plus.svg";
|
||||
case Icon::Pen: return "pen.svg";
|
||||
case Icon::Settings: return "gear.svg";
|
||||
case Icon::ChevronUp: return "chevron-up.svg";
|
||||
case Icon::ChevronDown: return "chevron-down.svg";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +76,10 @@ static constexpr Icon kAllIcons[] = {
|
||||
Icon::ToolSelect, Icon::ToolMove, Icon::ToolRotate, Icon::ToolScale,
|
||||
Icon::WorldSpace, Icon::LocalSpace, Icon::Grid, Icon::Antialias,
|
||||
Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad,
|
||||
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
|
||||
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
|
||||
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
|
||||
Icon::ChevronUp, Icon::ChevronDown,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -40,6 +40,24 @@ enum class Icon {
|
||||
LayoutHSplit, // two panels side-by-side
|
||||
LayoutVSplit, // two panels top/bottom
|
||||
LayoutQuad, // four panels 2×2
|
||||
// Timeline transport
|
||||
SkipBack, // go to first frame (|◀)
|
||||
StepBack, // previous frame (◀◀)
|
||||
PlayBack, // play in reverse (◀)
|
||||
Play, // play (▶)
|
||||
Pause, // pause (⏸)
|
||||
StepForward, // next frame (▶▶)
|
||||
SkipEnd, // go to last frame (▶|)
|
||||
Loop, // loop toggle (↻)
|
||||
Bounce, // bounce/ping-pong (↔)
|
||||
// General actions
|
||||
Refresh, // reload / refresh
|
||||
FilePlus, // create new file
|
||||
Pen, // edit / edit target
|
||||
Settings, // gear / viewport options
|
||||
// List reordering
|
||||
ChevronUp, // move item up
|
||||
ChevronDown, // move item down
|
||||
};
|
||||
|
||||
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_win32.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <filesystem>
|
||||
|
||||
// Forward declare message handler from imgui_impl_win32.cpp
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
@@ -93,6 +94,29 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei
|
||||
// With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any
|
||||
// ImGui window is focused, which would block all viewport hotkeys (F, A, etc.).
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
// Dock only while Shift is held during a title-bar/tab drag (the dock
|
||||
// position overlay appears then); a plain drag just moves the window,
|
||||
// so casual rearranging can't accidentally re-dock panels.
|
||||
io.ConfigDockingWithShift = true;
|
||||
// Windows move only via their title bar/tab, never by dragging empty
|
||||
// content. Without this, a press-drag that a content widget doesn't
|
||||
// capture falls through to ImGui's "drag body to move window" behaviour —
|
||||
// which, in the node-editor canvas (whose background button doesn't
|
||||
// reliably claim the drag in this nested-child embedding), meant dragging
|
||||
// in the graph moved the whole Material Editor window. Title-bar dragging
|
||||
// and Shift-to-dock are unaffected; this is also the desired behaviour for
|
||||
// every other content area (viewports, timelines) where a content drag
|
||||
// must never move the host window.
|
||||
io.ConfigWindowsMoveFromTitleBarOnly = true;
|
||||
|
||||
// Store imgui.ini in %APPDATA%\UsdLayerManager\ so it doesn't litter the CWD.
|
||||
if (const char* appData = getenv("APPDATA")) {
|
||||
std::filesystem::path iniDir =
|
||||
std::filesystem::path(appData) / "UsdLayerManager";
|
||||
std::filesystem::create_directories(iniDir);
|
||||
m_iniFilePath = (iniDir / "imgui.ini").string();
|
||||
io.IniFilename = m_iniFilePath.c_str();
|
||||
}
|
||||
|
||||
// Load Inter font.
|
||||
// The build directory copies the font as Inter.ttc; the install step renames
|
||||
@@ -106,7 +130,7 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei
|
||||
const char* tryPaths[] = { fontPath0.c_str(), fontPath1.c_str() };
|
||||
bool loaded = false;
|
||||
for (const char* p : tryPaths) {
|
||||
ImFont* f = io.Fonts->AddFontFromFileTTF(p, 16.0f, &fontConfig);
|
||||
ImFont* f = io.Fonts->AddFontFromFileTTF(p, 15.0f, &fontConfig);
|
||||
if (f) {
|
||||
LOG_INFO(std::string("Loaded Inter font from ") + p);
|
||||
loaded = true;
|
||||
|
||||
@@ -36,6 +36,7 @@ private:
|
||||
bool m_shouldClose;
|
||||
int m_width;
|
||||
int m_height;
|
||||
std::string m_iniFilePath; // persists for the lifetime of the ImGui context
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
#include "LayerPanel.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include "../core/commands/LayerCommands.h"
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
LayerPanel::LayerPanel()
|
||||
: m_layerManager(nullptr)
|
||||
, m_selectedLayerIndex(-1)
|
||||
, m_showCreateDialog(false) {
|
||||
m_newLayerPath[0] = '\0';
|
||||
m_newLayerName[0] = '\0';
|
||||
}
|
||||
|
||||
LayerPanel::~LayerPanel() {
|
||||
}
|
||||
|
||||
void LayerPanel::SetLayerManager(LayerManager* manager) {
|
||||
m_layerManager = manager;
|
||||
}
|
||||
|
||||
void LayerPanel::Render() {
|
||||
if (!m_layerManager) return;
|
||||
|
||||
// Header with buttons
|
||||
ImGui::Text("Layers");
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - 110);
|
||||
|
||||
if (ImGui::Button("Refresh")) {
|
||||
m_layerManager->Refresh();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add Layer")) {
|
||||
m_showCreateDialog = true;
|
||||
m_newLayerPath[0] = '\0';
|
||||
strcpy_s(m_newLayerName, "new_layer.usd");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Create layer dialog
|
||||
if (m_showCreateDialog) {
|
||||
ShowCreateLayerDialog();
|
||||
}
|
||||
|
||||
// Layer list
|
||||
auto layers = m_layerManager->GetLayerStack();
|
||||
|
||||
if (layers.empty()) {
|
||||
ImGui::TextDisabled("No layers loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
// Layer table
|
||||
if (ImGui::BeginTable("LayerTable", 4,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) {
|
||||
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20.0f);
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int i = 0; i < static_cast<int>(layers.size()); i++) {
|
||||
const auto& layerInfo = layers[i];
|
||||
ImGui::TableNextRow();
|
||||
|
||||
bool isSelected = (m_selectedLayerIndex == i);
|
||||
|
||||
// Selection column
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable("##select", isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) {
|
||||
m_selectedLayerIndex = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Name column
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImVec4 textColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
if (layerInfo.isMuted) {
|
||||
textColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
} else if (layerInfo.isRootLayer) {
|
||||
textColor = ImVec4(0.5f, 1.0f, 0.5f, 1.0f);
|
||||
} else if (layerInfo.isSessionLayer) {
|
||||
textColor = ImVec4(0.5f, 0.7f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
ImGui::TextColored(textColor, "%s", layerInfo.displayName.c_str());
|
||||
|
||||
if (ImGui::IsItemHovered() && !layerInfo.realPath.empty()) {
|
||||
ImGui::SetTooltip("%s\n%s", layerInfo.identifier.c_str(), layerInfo.realPath.c_str());
|
||||
}
|
||||
|
||||
// Mute toggle
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
bool muted = layerInfo.isMuted;
|
||||
ImGui::PushID(("mute_" + std::to_string(i)).c_str());
|
||||
if (ImGui::Checkbox("##muted", &muted)) {
|
||||
if (muted) {
|
||||
m_layerManager->MuteLayer(layerInfo.identifier);
|
||||
} else {
|
||||
m_layerManager->UnmuteLayer(layerInfo.identifier);
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Type column
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
if (layerInfo.isRootLayer) {
|
||||
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Root");
|
||||
} else if (layerInfo.isSessionLayer) {
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.7f, 1.0f, 1.0f), "Session");
|
||||
} else if (layerInfo.isAnonymous) {
|
||||
ImGui::Text("Anonymous");
|
||||
} else {
|
||||
ImGui::Text("Sublayer");
|
||||
}
|
||||
|
||||
// Context menu
|
||||
RenderLayerContextMenu(i);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
void LayerPanel::RenderLayerContextMenu(int layerIndex) {
|
||||
if (ImGui::BeginPopupContextItem(("layer_ctx_" + std::to_string(layerIndex)).c_str())) {
|
||||
auto layers = m_layerManager->GetLayerStack();
|
||||
if (layerIndex < 0 || layerIndex >= static_cast<int>(layers.size())) {
|
||||
ImGui::EndPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& info = layers[layerIndex];
|
||||
|
||||
if (ImGui::MenuItem(info.isMuted ? "Unmute" : "Mute")) {
|
||||
if (info.isMuted) {
|
||||
m_layerManager->UnmuteLayer(info.identifier);
|
||||
} else {
|
||||
m_layerManager->MuteLayer(info.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (!info.isRootLayer && !info.isSessionLayer) {
|
||||
if (ImGui::MenuItem("Move Up", nullptr, false, layerIndex > 0)) {
|
||||
if (m_commandHistory) {
|
||||
// Capture order before move.
|
||||
auto layers = m_layerManager->GetLayerStack();
|
||||
std::vector<std::string> before, after;
|
||||
for (auto& l : layers)
|
||||
if (!l.isRootLayer && !l.isSessionLayer)
|
||||
before.push_back(l.identifier);
|
||||
after = before;
|
||||
// Find index within sublayer-only list.
|
||||
int subIdx = -1;
|
||||
for (int i = 0; i < static_cast<int>(before.size()); ++i)
|
||||
if (before[i] == info.identifier) { subIdx = i; break; }
|
||||
if (subIdx > 0) std::swap(after[subIdx], after[subIdx - 1]);
|
||||
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
|
||||
m_layerManager, before, after));
|
||||
} else {
|
||||
m_layerManager->MoveSublayerUp(layerIndex);
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Move Down", nullptr, false, layerIndex < static_cast<int>(layers.size()) - 1)) {
|
||||
if (m_commandHistory) {
|
||||
auto layersNow = m_layerManager->GetLayerStack();
|
||||
std::vector<std::string> before, after;
|
||||
for (auto& l : layersNow)
|
||||
if (!l.isRootLayer && !l.isSessionLayer)
|
||||
before.push_back(l.identifier);
|
||||
after = before;
|
||||
int subIdx = -1;
|
||||
for (int i = 0; i < static_cast<int>(before.size()); ++i)
|
||||
if (before[i] == info.identifier) { subIdx = i; break; }
|
||||
if (subIdx >= 0 && subIdx + 1 < static_cast<int>(after.size()))
|
||||
std::swap(after[subIdx], after[subIdx + 1]);
|
||||
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
|
||||
m_layerManager, before, after));
|
||||
} else {
|
||||
m_layerManager->MoveSublayerDown(layerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem("Remove")) {
|
||||
if (m_commandHistory) {
|
||||
m_commandHistory->Push(std::make_unique<LayerRemoveCommand>(
|
||||
m_layerManager, layerIndex));
|
||||
} else {
|
||||
m_layerManager->RemoveSublayer(layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void LayerPanel::ShowCreateLayerDialog() {
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always);
|
||||
ImGui::OpenPopup("Create New Layer");
|
||||
|
||||
if (ImGui::BeginPopupModal("Create New Layer", &m_showCreateDialog)) {
|
||||
ImGui::Text("Layer Name:");
|
||||
ImGui::InputText("##name", m_newLayerName, sizeof(m_newLayerName));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Save Path:");
|
||||
ImGui::InputText("##path", m_newLayerPath, sizeof(m_newLayerPath));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse...")) {
|
||||
// TODO: File save dialog
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Create", ImVec2(120, 0))) {
|
||||
std::string path;
|
||||
if (m_newLayerPath[0] != '\0') {
|
||||
path = std::string(m_newLayerPath) + "/" + m_newLayerName;
|
||||
} else {
|
||||
path = m_newLayerName;
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
if (m_commandHistory) {
|
||||
m_commandHistory->Push(std::make_unique<LayerCreateCommand>(
|
||||
m_layerManager, "./" + path));
|
||||
} else {
|
||||
m_layerManager->CreateSublayer("./" + path);
|
||||
}
|
||||
m_showCreateDialog = false;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
m_showCreateDialog = false;
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/LayerManager.h"
|
||||
#include "../core/CommandHistory.h"
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
class LayerPanel {
|
||||
public:
|
||||
LayerPanel();
|
||||
~LayerPanel();
|
||||
|
||||
void SetLayerManager(LayerManager* manager);
|
||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||
void Render();
|
||||
|
||||
private:
|
||||
void RenderLayerContextMenu(int layerIndex);
|
||||
void ShowCreateLayerDialog();
|
||||
|
||||
LayerManager* m_layerManager;
|
||||
CommandHistory* m_commandHistory = nullptr;
|
||||
int m_selectedLayerIndex;
|
||||
bool m_showCreateDialog;
|
||||
char m_newLayerPath[256];
|
||||
char m_newLayerName[128];
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,323 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/CommandHistory.h"
|
||||
#include "../core/MaterialManager.h"
|
||||
#include "IconManager.h"
|
||||
#include "MaterialPreviewRenderer.h"
|
||||
#include "NodeThumbnailCache.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/valueTypeName.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <imgui_node_editor.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Node-graph editor for authoring UsdShade material networks, with a
|
||||
/// shader-ball preview rendered through Hydra.
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
MaterialEditorPanel();
|
||||
~MaterialEditorPanel();
|
||||
|
||||
void SetStage(pxr::UsdStageRefPtr stage);
|
||||
void SetMaterialManager(MaterialManager* mgr) { m_materialManager = mgr; }
|
||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
|
||||
|
||||
/// Forwarded to the shader-ball preview so it matches the main viewport's
|
||||
/// global color-correction settings.
|
||||
void SetColorCorrectionFromPrefs(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook);
|
||||
|
||||
/// Opens the material at pathStr in the graph work area, creating it (as a
|
||||
/// "Material" prim) if it doesn't exist yet. Also selects it in the browser.
|
||||
void OpenOrCreateMaterial(const std::string& pathStr);
|
||||
|
||||
/// Tracks the current selection (mirrors PropertyPanel/ViewportPanel's
|
||||
/// SetSelectedPrimPath) so the toolbar can offer "create + bind" / "bind"
|
||||
/// affordances, and so selecting a prim with an already-bound material
|
||||
/// auto-loads it into the canvas.
|
||||
void SetTargetPrimPath(const std::string& path);
|
||||
|
||||
/// Splitter-adjustable column widths, persisted in preferences.ini by
|
||||
/// Application. Setter clamps to the same range as the splitters.
|
||||
void GetColumnWidths(float& browserWidth, float& previewWidth) const {
|
||||
browserWidth = m_browserWidth;
|
||||
previewWidth = m_previewWidth;
|
||||
}
|
||||
void SetColumnWidths(float browserWidth, float previewWidth);
|
||||
|
||||
/// Whether toggling a node's pin display mode / revealing a hidden pin
|
||||
/// authors USD customData (persisted) or stays session-only. Pushed in by
|
||||
/// Application from AppPreferences, live-updated when the user flips the
|
||||
/// checkbox in Preferences -> Material.
|
||||
void SetKeepGraphNodeViewSettingsInUsd(bool value) { m_keepGraphNodeViewSettingsInUsd = value; }
|
||||
|
||||
void Render();
|
||||
|
||||
private:
|
||||
/// Resolved endpoint of a rendered pin, keyed by its ax::NodeEditor PinId.
|
||||
struct PinInfo {
|
||||
pxr::SdfPath nodePath;
|
||||
std::string name;
|
||||
bool isOutput = false;
|
||||
pxr::SdfValueTypeName typeName;
|
||||
/// True for the collapsed "more pins" connector nub rendered on a
|
||||
/// node's title row when it's hiding unconnected pins (Hypershade
|
||||
/// style) — name/typeName are unset. Connecting to/from it opens
|
||||
/// RenderPinPickMenu() to choose which actual pin to wire, instead of
|
||||
/// creating the connection immediately. See HandleCreateAndDelete().
|
||||
bool isMeta = false;
|
||||
};
|
||||
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
|
||||
struct LinkInfo {
|
||||
pxr::SdfPath destNode;
|
||||
std::string destInput;
|
||||
};
|
||||
|
||||
/// Hypershade-style per-node pin display state. Always the render-time
|
||||
/// source of truth (never resync-clobbered); optionally mirrored into USD
|
||||
/// customData when m_keepGraphNodeViewSettingsInUsd is set. See
|
||||
/// GetPinDisplayOverride()/IsPinVisible().
|
||||
struct PinDisplayOverride {
|
||||
PinDisplayMode mode = PinDisplayMode::All;
|
||||
};
|
||||
/// A connection dragged onto/from a collapsed meta pin, awaiting the user
|
||||
/// picking which actual pin on nodePath/isOutput to wire up. otherPin is
|
||||
/// the already-resolved endpoint on the far side of the link (itself meta
|
||||
/// if the user dragged directly between two collapsed nubs, in which case
|
||||
/// picking chains into a second RenderPinPickMenu() for that side).
|
||||
struct PendingPinPick {
|
||||
pxr::SdfPath nodePath;
|
||||
bool isOutput = false;
|
||||
PinInfo otherPin;
|
||||
};
|
||||
|
||||
void RenderToolbar();
|
||||
/// Property editor for the node selected in the graph, shown below the
|
||||
/// shader-ball preview. Values apply live while a widget is being
|
||||
/// dragged; one undoable command is pushed when the edit ends.
|
||||
void RenderSelectedNodeProperties();
|
||||
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||
const pxr::UsdPrim& prim);
|
||||
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
|
||||
/// A "Color Space" row (raw/sRGB/auto/unset) under an Asset-typed input,
|
||||
/// authoring the attribute's colorSpace metadata (UsdAttribute::
|
||||
/// SetColorSpace) — the generic, renderer-agnostic mechanism Hydra reads
|
||||
/// for texture color space (see UsdImagingDataSourceAttributeColorSpace),
|
||||
/// and the only one available for shader types with no separate
|
||||
/// sourceColorSpace input, e.g. MaterialX image nodes.
|
||||
void RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||
const pxr::UsdPrim& prim);
|
||||
/// Hypershade-style browser: lists every material on the stage; the
|
||||
/// selected one is loaded into the graph work area via "Show Graph"
|
||||
/// (or double-click).
|
||||
void RenderMaterialBrowser();
|
||||
/// Creates a uniquely-named empty material under /Materials and opens it.
|
||||
void CreateNewMaterial();
|
||||
/// Preset graphs for the open material (one undoable command each):
|
||||
/// UsdPreviewSurface + UsdUVTextures fed by an st primvar reader, or a
|
||||
/// MaterialX standard_surface + image nodes fed by a texcoord node.
|
||||
void CreateUsdPresetGraph();
|
||||
void CreateMaterialXPresetGraph();
|
||||
void CreateArnoldPresetGraph();
|
||||
void RenderNodeGraphCanvas();
|
||||
void RenderPreviewPanel();
|
||||
void HandleCreateAndDelete();
|
||||
/// Nuke-style TAB popup: type-to-filter shader node list; Enter or click
|
||||
/// creates the highlighted node at canvasPos. When linkSource is non-null
|
||||
/// (a connection was dragged onto empty canvas), the list is filtered to
|
||||
/// nodes with a type-connectable pin and the chosen node is auto-wired to
|
||||
/// linkSource.
|
||||
void RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource = nullptr);
|
||||
/// Creates the node and returns its prim path (empty on failure).
|
||||
pxr::SdfPath CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos);
|
||||
/// Nudges a creation point right until the new node's estimated rectangle
|
||||
/// clears every existing node's rendered rectangle. Overlapping nodes
|
||||
/// fight over the editor's hit test: the buried one becomes unmovable and
|
||||
/// its pins unhoverable. Must be called with the editor context current.
|
||||
ImVec2 FindFreeCanvasSpot(ImVec2 desired, const std::string& shaderId) const;
|
||||
/// Binds an existing material to targetPath, undoably, restoring whatever
|
||||
/// direct binding (if any) targetPath had before.
|
||||
void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath);
|
||||
/// Creates a new material named after m_targetPrimPath under /Materials,
|
||||
/// binds it there, and opens it in the canvas.
|
||||
void CreateAndBindMaterialForTarget();
|
||||
void CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput);
|
||||
void DeleteNode(const pxr::SdfPath& nodePath);
|
||||
void DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput);
|
||||
/// Resolved on-disk path of a texture node's image (its Asset-typed `file`
|
||||
/// input), or empty if unauthored/unresolvable. Feeds the thumbnail cache.
|
||||
std::string ResolveTextureFilePath(const ShaderGraphNode& node) const;
|
||||
void SyncFromUsd();
|
||||
void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
|
||||
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
||||
|
||||
/// Seeds m_pinDisplayOverrides for any node path not already present
|
||||
/// (i.e. not touched yet this session) from the just-synced m_graph's
|
||||
/// pinDisplayMode (customData-backed). Never overwrites an existing
|
||||
/// entry, so in-session toggles survive resyncs.
|
||||
void SeedPinDisplayOverrides();
|
||||
/// Creates the entry (defaulted to All) on first access.
|
||||
PinDisplayOverride& GetPinDisplayOverride(const pxr::SdfPath& nodePath);
|
||||
/// True if pin should be drawn this frame: All mode shows everything;
|
||||
/// Connected mode shows only linked pins (linked pins are never hidden —
|
||||
/// Link() unconditionally references pin IDs that must have been drawn —
|
||||
/// so a hidden pin becomes visible the moment it's connected, via
|
||||
/// RenderPinPickMenu()); Collapsed mode hides every real pin, linked or
|
||||
/// not, in favor of the single connector nub per side (see
|
||||
/// EffectivePinId(), which redirects existing links to that nub).
|
||||
bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const;
|
||||
/// Cycles a node's display mode: All -> Connected -> Collapsed -> All.
|
||||
void CyclePinDisplayMode(const pxr::SdfPath& nodePath);
|
||||
/// Sets every node currently in m_graph to mode (toolbar "set all" button).
|
||||
void ApplyPinDisplayModeToAllNodes(PinDisplayMode mode);
|
||||
/// Authors the node's current PinDisplayOverride into USD customData via
|
||||
/// an undoable command, mirroring PersistNodePosition. No-op unless
|
||||
/// m_keepGraphNodeViewSettingsInUsd is set.
|
||||
void PersistPinDisplayState(const pxr::SdfPath& nodePath);
|
||||
/// PinId to use when drawing a link touching (nodePath, pinName,
|
||||
/// isOutput): the pin's own id normally, or that node's collapsed meta
|
||||
/// nub id when the node is in Collapsed mode (so existing links visually
|
||||
/// converge on the nub instead of referencing an undrawn real pin).
|
||||
uintptr_t EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
||||
/// Renders the deferred "choose pin" popup for m_pendingPinPick (Maya
|
||||
/// Hypershade style): a connection was dragged onto/from a collapsed meta
|
||||
/// pin, and the user now picks which actual attribute it wires to. Must
|
||||
/// be called from within the NE::Suspend()/Resume() block, like the
|
||||
/// other popups.
|
||||
void RenderPinPickMenu();
|
||||
/// Finishes a pick: creates the real connection to/from chosen, unless
|
||||
/// the far side of the pending pick was itself a meta pin, in which case
|
||||
/// this chains into a second pick for that side instead.
|
||||
void CompletePinPick(const PinInfo& chosen);
|
||||
|
||||
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
|
||||
const char* data, size_t size,
|
||||
ax::NodeEditor::SaveReasonFlags reason,
|
||||
void* userPointer);
|
||||
|
||||
ax::NodeEditor::EditorContext* m_editorContext = nullptr;
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
MaterialManager* m_materialManager = nullptr;
|
||||
CommandHistory* m_commandHistory = nullptr;
|
||||
IconManager* m_iconManager = nullptr;
|
||||
|
||||
/// Last color correction pushed via SetColorCorrectionFromPrefs, cached
|
||||
/// so RenderNodeGraphCanvas can pass it to NodeThumbnailCache every frame
|
||||
/// (keeps in-canvas node thumbnails matching m_preview/the app global).
|
||||
int m_ccMode = 1; // ColorCorrectionMode cast to int (1 = sRGB), mirrors AppPreferences' default
|
||||
std::string m_ocioDisplay;
|
||||
std::string m_ocioView;
|
||||
std::string m_ocioColorSpace;
|
||||
std::string m_ocioLook;
|
||||
|
||||
/// Material currently shown in the graph work area (empty = none).
|
||||
pxr::SdfPath m_materialPath;
|
||||
/// Material highlighted in the browser list; becomes m_materialPath when
|
||||
/// the user clicks "Show Graph" (or double-clicks the entry).
|
||||
pxr::SdfPath m_browserSelection;
|
||||
/// Currently-selected prim, tracked for the "create + bind" / "bind"
|
||||
/// toolbar affordances; empty when nothing (or a non-prim path) is selected.
|
||||
pxr::SdfPath m_targetPrimPath;
|
||||
|
||||
ShaderGraphSnapshot m_graph;
|
||||
/// NodeId (uintptr_t) -> prim path, for nodes already known to the editor
|
||||
/// this session (seeded position, so re-syncing won't fight live drags).
|
||||
std::unordered_map<uintptr_t, pxr::SdfPath> m_nodeIdToPath;
|
||||
/// Rebuilt every frame from the current m_graph while rendering.
|
||||
std::unordered_map<uintptr_t, PinInfo> m_pinIdToInfo;
|
||||
std::unordered_map<uintptr_t, LinkInfo> m_linkIdToInfo;
|
||||
|
||||
MaterialPreviewRenderer m_preview;
|
||||
/// Per-node in-canvas thumbnails (texture images + material shader balls).
|
||||
NodeThumbnailCache m_thumbnails;
|
||||
|
||||
ImVec2 m_pendingCreateNodePos{0.0f, 0.0f};
|
||||
|
||||
/// Alt+RMB drag distance not yet converted into a discrete wheel-zoom
|
||||
/// step (the node editor only zooms in wheel increments).
|
||||
float m_zoomDragAccum = 0.0f;
|
||||
|
||||
char m_nodeSearchBuf[128] = "";
|
||||
int m_nodeSearchSelected = 0;
|
||||
|
||||
/// Nuke-style drag-to-create: set when a connection was dragged from a pin
|
||||
/// onto empty canvas. m_openLinkDragMenu requests the create-node popup on
|
||||
/// the next frame (deferred so OpenPopup runs while the editor is
|
||||
/// suspended); m_linkDragSourcePin is the pin to auto-wire the new node to.
|
||||
bool m_openLinkDragMenu = false;
|
||||
PinInfo m_linkDragSourcePin;
|
||||
|
||||
/// Hypershade-style pin display state, keyed by node path string. Always
|
||||
/// the render-time source of truth; see PinDisplayOverride.
|
||||
std::unordered_map<std::string, PinDisplayOverride> m_pinDisplayOverrides;
|
||||
/// Whether node toggles author USD customData (see
|
||||
/// SetKeepGraphNodeViewSettingsInUsd).
|
||||
bool m_keepGraphNodeViewSettingsInUsd = false;
|
||||
/// Deferred "choose pin" popup request: set from HandleCreateAndDelete()
|
||||
/// when a link is dropped on/dragged from a collapsed meta pin, actually
|
||||
/// opened after NE::Suspend(), like m_openLinkDragMenu.
|
||||
bool m_openPinPickMenu = false;
|
||||
PendingPinPick m_pendingPinPick;
|
||||
|
||||
/// Toolbar toggle: whether node thumbnails (texture images / material
|
||||
/// shader-ball renders) are shown at all. Off skips the reserved space
|
||||
/// and the decode/render work entirely, not just the visual.
|
||||
bool m_showNodeThumbnails = true;
|
||||
/// Toolbar "set all nodes" pin-display button: the mode last broadcast
|
||||
/// to every node (and the icon/tooltip shown), independent of what any
|
||||
/// individual node is actually in after the user tweaks one by hand.
|
||||
PinDisplayMode m_toolbarPinDisplayMode = PinDisplayMode::All;
|
||||
|
||||
/// Column widths of the browser (left) and preview (right) sections,
|
||||
/// user-adjustable via the splitters between them and the canvas.
|
||||
float m_browserWidth = 220.0f;
|
||||
float m_previewWidth = 320.0f;
|
||||
|
||||
/// Fraction of the browser column's height given to the materials list;
|
||||
/// the create-node list takes the rest. User-adjustable via the horizontal
|
||||
/// splitter between them.
|
||||
float m_browserListRatio = 0.45f;
|
||||
|
||||
/// Inline-rename state for the materials list (double-click a row to start;
|
||||
/// mirrors SceneHierarchyPanel): the material being renamed (empty = none),
|
||||
/// the edit buffer, and a one-frame flag to focus the InputText on start.
|
||||
pxr::SdfPath m_renamingMaterial;
|
||||
char m_materialRenameBuf[128] = "";
|
||||
bool m_materialRenameJustStarted = false;
|
||||
|
||||
/// Search text of the browser's persistent create-node list.
|
||||
char m_browserNodeSearchBuf[128] = "";
|
||||
/// Shader id clicked in the browser's create-node list; created at the
|
||||
/// canvas view center on the next RenderNodeGraphCanvas (ScreenToCanvas
|
||||
/// is only valid inside the editor's Begin/End).
|
||||
std::string m_pendingCreateShaderId;
|
||||
|
||||
/// Debug event-trace state (see the logging block at the end of
|
||||
/// RenderNodeGraphCanvas): whether the last LMB press landed on a node
|
||||
/// rect, whether that press's drag-start has been logged yet, and the
|
||||
/// editor NodeIds hit at press time (their live positions are logged on
|
||||
/// every drag-move so a node that stops following the mouse is visible).
|
||||
bool m_debugPressOnNode = false;
|
||||
bool m_debugDragLogged = false;
|
||||
std::vector<uintptr_t> m_debugDragNodeIds;
|
||||
|
||||
/// First node currently selected in the graph editor (empty = none);
|
||||
/// drives the properties section under the preview.
|
||||
pxr::SdfPath m_selectedNodePath;
|
||||
/// Value/authored-state of the input being edited, captured when its
|
||||
/// widget activates, so the commit command can restore it on undo. Only
|
||||
/// one ImGui widget can be active at a time, so one slot suffices.
|
||||
pxr::VtValue m_preEditValue;
|
||||
bool m_preEditWasAuthored = false;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,628 @@
|
||||
#include "MaterialPreviewRenderer.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include "../utils/PathUtils.h"
|
||||
#include <pxr/base/tf/stringUtils.h>
|
||||
#include <pxr/usd/usdGeom/sphere.h>
|
||||
#include <pxr/usd/usdGeom/cube.h>
|
||||
#include <pxr/usd/usdGeom/cylinder.h>
|
||||
#include <pxr/usd/usdGeom/mesh.h>
|
||||
#include <pxr/usd/usdGeom/primvarsAPI.h>
|
||||
#include <pxr/usd/usdGeom/scope.h>
|
||||
#include <pxr/usd/usdGeom/metrics.h>
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/usd/usdVol/volume.h>
|
||||
#include <pxr/usd/usdVol/openVDBAsset.h>
|
||||
#include <pxr/base/vt/array.h>
|
||||
#include <pxr/base/gf/vec2f.h>
|
||||
#include <pxr/base/gf/vec3f.h>
|
||||
#include <pxr/usd/usdShade/materialBindingAPI.h>
|
||||
#include <pxr/usd/usdShade/material.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||
#include <pxr/usd/usdLux/distantLight.h>
|
||||
#include <pxr/usd/usdLux/domeLight.h>
|
||||
#include <pxr/usd/usdLux/lightAPI.h>
|
||||
#include <pxr/usd/usdLux/tokens.h>
|
||||
#include <pxr/usd/usdGeom/xformCommonAPI.h>
|
||||
#include <pxr/usd/usd/references.h>
|
||||
#include <pxr/usd/usd/editContext.h>
|
||||
#include <pxr/usd/sdf/assetPath.h>
|
||||
#include <pxr/usd/sdf/primSpec.h>
|
||||
#include <imgui.h>
|
||||
#include <filesystem>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
namespace {
|
||||
const pxr::SdfPath kShapesRootPath("/Preview/Shapes");
|
||||
const pxr::SdfPath kDistantLightPath("/Preview/Light");
|
||||
const pxr::SdfPath kDomeLightPath("/Preview/DomeLight");
|
||||
|
||||
/// Selectable preview geometry; prim name is a child of kShapesRootPath.
|
||||
/// Shapes backed by an asset file are only created when the file exists.
|
||||
/// frameFit: camera framing margin — the camera frames the shape's bounding
|
||||
/// box, and a cube fills its box completely while a sphere only inscribes it,
|
||||
/// so boxy shapes need a larger margin to read at a similar visual size.
|
||||
struct ShapePreset {
|
||||
const char* label;
|
||||
const char* primName;
|
||||
double frameFit;
|
||||
};
|
||||
const ShapePreset kShapePresets[] = {
|
||||
{"Sphere", "Sphere", 1.6},
|
||||
{"Cube", "Cube", 2.1},
|
||||
{"Cylinder", "Cylinder", 1.8},
|
||||
{"Teapot", "Teapot", 1.6},
|
||||
{"Hair", "Hair", 1.6},
|
||||
{"Cloud Volume", "Cloud", 1.6},
|
||||
};
|
||||
constexpr int kShapePresetCount = static_cast<int>(sizeof(kShapePresets) / sizeof(kShapePresets[0]));
|
||||
|
||||
/// Scratch UsdPreviewSurface authored inside the (referenced) material scope
|
||||
/// when previewing a non-terminal node output — child of the material so the
|
||||
/// connection stays encapsulated.
|
||||
const pxr::TfToken kNodePreviewShaderName("__uslm_node_preview");
|
||||
|
||||
struct LightPreset {
|
||||
const char* label;
|
||||
const char* file; ///< under resources/hdri/ (CC0, Poly Haven; fetched by CMake)
|
||||
};
|
||||
const LightPreset kLightPresets[] = {
|
||||
{"External", "kloofendal_48d_partly_cloudy_puresky_1k.exr"},
|
||||
{"Room", "lebombo_1k.exr"},
|
||||
{"Interior", "artist_workshop_1k.exr"},
|
||||
{"Sunset", "venice_sunset_1k.exr"},
|
||||
};
|
||||
constexpr int kLightPresetCount = static_cast<int>(sizeof(kLightPresets) / sizeof(kLightPresets[0]));
|
||||
|
||||
/// Authors a polygonal UsdGeomMesh from prebuilt arrays: vertex-interpolated
|
||||
/// normals + a `st` texCoord2f primvar, no subdivision, single-sided. Shared by
|
||||
/// every preview shape so they all light and texture identically.
|
||||
void DefineUvMesh(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path,
|
||||
const pxr::VtVec3fArray& points, const pxr::VtVec3fArray& normals,
|
||||
const pxr::VtVec2fArray& st, const pxr::VtIntArray& fvCounts,
|
||||
const pxr::VtIntArray& fvIndices,
|
||||
const pxr::GfVec3f& extentMin, const pxr::GfVec3f& extentMax) {
|
||||
pxr::UsdGeomMesh mesh = pxr::UsdGeomMesh::Define(stage, path);
|
||||
mesh.CreatePointsAttr(pxr::VtValue(points));
|
||||
mesh.CreateNormalsAttr(pxr::VtValue(normals));
|
||||
mesh.SetNormalsInterpolation(pxr::UsdGeomTokens->vertex);
|
||||
mesh.CreateFaceVertexCountsAttr(pxr::VtValue(fvCounts));
|
||||
mesh.CreateFaceVertexIndicesAttr(pxr::VtValue(fvIndices));
|
||||
// Polygonal (not subdivided) so the authored smooth normals and UVs are used
|
||||
// verbatim. Single-sided (the default): a closed shape never shows a
|
||||
// backface, and double-sided shading runs a view-dependent normal flip that
|
||||
// corrupts the mirror-reflection vector near the silhouette.
|
||||
mesh.CreateSubdivisionSchemeAttr(pxr::VtValue(pxr::UsdGeomTokens->none));
|
||||
mesh.CreateExtentAttr(pxr::VtValue(pxr::VtVec3fArray{extentMin, extentMax}));
|
||||
pxr::UsdGeomPrimvarsAPI(mesh).CreatePrimvar(
|
||||
pxr::TfToken("st"), pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->vertex)
|
||||
.Set(st);
|
||||
}
|
||||
|
||||
/// Authors a UV-mapped mesh sphere (Y-up) at `path` with vertex normals and a
|
||||
/// `st` texCoord2f primvar. The implicit UsdGeomSphere carries no texture
|
||||
/// coordinates (GeomUtilSphereMeshGenerator emits only points/normals/topology),
|
||||
/// so a UsdUVTexture fed by a UsdPrimvarReader_float2("st") would only ever read
|
||||
/// the reader's fallback and paint a flat colour. This mesh gives the shader
|
||||
/// ball real UVs so textures map across every render delegate.
|
||||
void CreateUvSphere(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path, double radius) {
|
||||
constexpr int rings = 48; // latitude bands (theta 0..pi)
|
||||
constexpr int sectors = 96; // longitude bands (phi 0..2pi)
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
const float r = static_cast<float>(radius);
|
||||
|
||||
pxr::VtVec3fArray points, normals;
|
||||
pxr::VtVec2fArray st;
|
||||
points.reserve((rings + 1) * (sectors + 1));
|
||||
normals.reserve((rings + 1) * (sectors + 1));
|
||||
st.reserve((rings + 1) * (sectors + 1));
|
||||
// A duplicated seam column (sectors+1 wide) gives distinct u=0 and u=1
|
||||
// vertices so the texture wraps without a smear across the seam.
|
||||
for (int i = 0; i <= rings; ++i) {
|
||||
const float v = static_cast<float>(i) / rings;
|
||||
const float th = v * static_cast<float>(kPi);
|
||||
const float sinT = std::sin(th), cosT = std::cos(th);
|
||||
for (int j = 0; j <= sectors; ++j) {
|
||||
const float u = static_cast<float>(j) / sectors;
|
||||
const float phi = u * 2.0f * static_cast<float>(kPi);
|
||||
const pxr::GfVec3f n(sinT * std::cos(phi), cosT, sinT * std::sin(phi));
|
||||
points.push_back(n * r);
|
||||
normals.push_back(n);
|
||||
st.push_back(pxr::GfVec2f(u, 1.0f - v)); // flip V so image top → sphere top
|
||||
}
|
||||
}
|
||||
|
||||
pxr::VtIntArray fvCounts, fvIndices;
|
||||
const int stride = sectors + 1;
|
||||
for (int i = 0; i < rings; ++i)
|
||||
for (int j = 0; j < sectors; ++j) {
|
||||
const int a = i * stride + j;
|
||||
fvCounts.push_back(4);
|
||||
// Wind CCW as seen from outside so the (single-sided) front face and
|
||||
// the outward vertex normals agree; a reversed winding would either
|
||||
// cull the visible surface or flip its shading normal.
|
||||
fvIndices.push_back(a);
|
||||
fvIndices.push_back(a + 1);
|
||||
fvIndices.push_back(a + stride + 1);
|
||||
fvIndices.push_back(a + stride);
|
||||
}
|
||||
|
||||
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
|
||||
pxr::GfVec3f(-r, -r, -r), pxr::GfVec3f(r, r, r));
|
||||
}
|
||||
|
||||
/// UV-mapped box: 24 vertices (4 per face) so every face gets its own outward
|
||||
/// normal and a full 0..1 UV square. halfSize is the half-edge length.
|
||||
void CreateUvCube(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path, double halfSize) {
|
||||
const float h = static_cast<float>(halfSize);
|
||||
pxr::VtVec3fArray points, normals;
|
||||
pxr::VtVec2fArray st;
|
||||
pxr::VtIntArray fvCounts, fvIndices;
|
||||
|
||||
// p0..p3 are wound CCW as seen from outside, so the front face agrees with n.
|
||||
auto addFace = [&](const pxr::GfVec3f& p0, const pxr::GfVec3f& p1,
|
||||
const pxr::GfVec3f& p2, const pxr::GfVec3f& p3,
|
||||
const pxr::GfVec3f& n) {
|
||||
const int base = static_cast<int>(points.size());
|
||||
points.push_back(p0); points.push_back(p1); points.push_back(p2); points.push_back(p3);
|
||||
for (int k = 0; k < 4; ++k) normals.push_back(n);
|
||||
st.push_back(pxr::GfVec2f(0, 0)); st.push_back(pxr::GfVec2f(1, 0));
|
||||
st.push_back(pxr::GfVec2f(1, 1)); st.push_back(pxr::GfVec2f(0, 1));
|
||||
fvCounts.push_back(4);
|
||||
fvIndices.push_back(base + 0); fvIndices.push_back(base + 1);
|
||||
fvIndices.push_back(base + 2); fvIndices.push_back(base + 3);
|
||||
};
|
||||
addFace({ h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h}, { 1, 0, 0}); // +X
|
||||
addFace({-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h}, {-1, 0, 0}); // -X
|
||||
addFace({-h, h,-h}, {-h, h, h}, { h, h, h}, { h, h,-h}, { 0, 1, 0}); // +Y
|
||||
addFace({-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h}, { 0,-1, 0}); // -Y
|
||||
addFace({-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, { 0, 0, 1}); // +Z
|
||||
addFace({ h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h}, { 0, 0,-1}); // -Z
|
||||
|
||||
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
|
||||
pxr::GfVec3f(-h, -h, -h), pxr::GfVec3f(h, h, h));
|
||||
}
|
||||
|
||||
/// UV-mapped cylinder (Y axis): radial side wall + two end caps. Side normals
|
||||
/// are radial, caps ±Y; UVs wrap the wall (u around, v along height) and map
|
||||
/// each cap as a disk.
|
||||
void CreateUvCylinder(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path,
|
||||
double radius, double halfHeight) {
|
||||
constexpr int sectors = 96;
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
const float R = static_cast<float>(radius);
|
||||
const float hy = static_cast<float>(halfHeight);
|
||||
|
||||
pxr::VtVec3fArray points, normals;
|
||||
pxr::VtVec2fArray st;
|
||||
pxr::VtIntArray fvCounts, fvIndices;
|
||||
|
||||
// Side wall: sectors+1 columns (duplicated seam for a clean u wrap), each a
|
||||
// bottom+top pair interleaved so column j owns indices 2j (bottom), 2j+1 (top).
|
||||
for (int j = 0; j <= sectors; ++j) {
|
||||
const float u = static_cast<float>(j) / sectors;
|
||||
const float phi = u * 2.0f * static_cast<float>(kPi);
|
||||
const float c = std::cos(phi), s = std::sin(phi);
|
||||
points.push_back(pxr::GfVec3f(R * c, -hy, R * s));
|
||||
normals.push_back(pxr::GfVec3f(c, 0, s));
|
||||
st.push_back(pxr::GfVec2f(u, 0));
|
||||
points.push_back(pxr::GfVec3f(R * c, hy, R * s));
|
||||
normals.push_back(pxr::GfVec3f(c, 0, s));
|
||||
st.push_back(pxr::GfVec2f(u, 1));
|
||||
}
|
||||
for (int j = 0; j < sectors; ++j) {
|
||||
const int b0 = 2 * j, t0 = 2 * j + 1, b1 = 2 * (j + 1), t1 = 2 * (j + 1) + 1;
|
||||
fvCounts.push_back(4); // b_j, t_j, t_{j+1}, b_{j+1} → outward radial
|
||||
fvIndices.push_back(b0); fvIndices.push_back(t0);
|
||||
fvIndices.push_back(t1); fvIndices.push_back(b1);
|
||||
}
|
||||
|
||||
// End caps as triangle fans around a center vertex; ring vertices carry the
|
||||
// cap normal (not the radial one) and disk UVs.
|
||||
auto addCap = [&](float y, float ny) {
|
||||
const int center = static_cast<int>(points.size());
|
||||
points.push_back(pxr::GfVec3f(0, y, 0));
|
||||
normals.push_back(pxr::GfVec3f(0, ny, 0));
|
||||
st.push_back(pxr::GfVec2f(0.5f, 0.5f));
|
||||
const int ringBase = static_cast<int>(points.size());
|
||||
for (int j = 0; j < sectors; ++j) {
|
||||
const float phi = static_cast<float>(j) / sectors * 2.0f * static_cast<float>(kPi);
|
||||
const float c = std::cos(phi), s = std::sin(phi);
|
||||
points.push_back(pxr::GfVec3f(R * c, y, R * s));
|
||||
normals.push_back(pxr::GfVec3f(0, ny, 0));
|
||||
st.push_back(pxr::GfVec2f(0.5f + 0.5f * c, 0.5f + 0.5f * s));
|
||||
}
|
||||
for (int j = 0; j < sectors; ++j) {
|
||||
const int a = ringBase + j, b = ringBase + (j + 1) % sectors;
|
||||
fvCounts.push_back(3);
|
||||
fvIndices.push_back(center);
|
||||
// Wind so the tri normal matches the cap direction (+Y vs -Y).
|
||||
if (ny > 0) { fvIndices.push_back(b); fvIndices.push_back(a); }
|
||||
else { fvIndices.push_back(a); fvIndices.push_back(b); }
|
||||
}
|
||||
};
|
||||
addCap( hy, 1.0f);
|
||||
addCap(-hy, -1.0f);
|
||||
|
||||
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
|
||||
pxr::GfVec3f(-R, -hy, -R), pxr::GfVec3f(R, hy, R));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MaterialPreviewRenderer::EnsureInitialized() {
|
||||
if (m_initialized) return;
|
||||
|
||||
m_previewStage = pxr::UsdStage::CreateInMemory();
|
||||
// Everything authored here (sphere, lights, reference) must go to the
|
||||
// root layer: the session layer is reserved for node-preview overrides
|
||||
// and is wiped wholesale on every SetMaterial.
|
||||
m_previewStage->SetEditTarget(pxr::UsdEditTarget(m_previewStage->GetRootLayer()));
|
||||
// Pin the up axis rather than inheriting the site fallback: the light
|
||||
// rig, dome-pole compensation, and camera framing all assume Y-up.
|
||||
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
|
||||
|
||||
// All selectable preview shapes live under one scope; the material is
|
||||
// bound on the scope so it inherits to whichever shape is active, and
|
||||
// ApplyPreviewShape keeps exactly one child active.
|
||||
pxr::UsdGeomScope::Define(m_previewStage, kShapesRootPath);
|
||||
|
||||
// UV-mapped meshes (not implicit UsdGeom prims) so textured materials —
|
||||
// e.g. a UsdUVTexture wired into diffuseColor via a UsdPrimvarReader "st" —
|
||||
// map onto the preview geometry instead of sampling a single texel. The
|
||||
// implicit primitives generate no `st` primvar, so they can't show textures.
|
||||
CreateUvSphere(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Sphere")), 1.0);
|
||||
CreateUvCube(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cube")), 0.6);
|
||||
CreateUvCylinder(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cylinder")), 0.7, 0.8);
|
||||
|
||||
// Asset-backed shapes, created only when their file is bundled.
|
||||
for (const char* name : {"Teapot", "Hair"}) {
|
||||
const std::string file = ResourcePath(
|
||||
std::string("resources/preview/") + pxr::TfStringToLower(name) + ".usdc");
|
||||
if (!std::filesystem::exists(file)) continue;
|
||||
pxr::UsdPrim prim = m_previewStage->DefinePrim(
|
||||
kShapesRootPath.AppendChild(pxr::TfToken(name)));
|
||||
prim.GetReferences().AddReference(file);
|
||||
}
|
||||
|
||||
const std::string vdbFile = ResourcePath("resources/vdb/bunny_cloud.vdb");
|
||||
if (std::filesystem::exists(vdbFile)) {
|
||||
pxr::UsdVolVolume cloud = pxr::UsdVolVolume::Define(
|
||||
m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cloud")));
|
||||
pxr::UsdVolOpenVDBAsset density = pxr::UsdVolOpenVDBAsset::Define(
|
||||
m_previewStage, cloud.GetPath().AppendChild(pxr::TfToken("density")));
|
||||
density.CreateFilePathAttr().Set(pxr::SdfAssetPath(vdbFile));
|
||||
density.CreateFieldNameAttr().Set(pxr::TfToken("density"));
|
||||
cloud.CreateFieldRelationship(pxr::TfToken("density"), density.GetPath());
|
||||
}
|
||||
|
||||
// Storm renders fine with zero authored lights (it injects a GL headlight
|
||||
// as a fallback), but proper Hydra delegates like Embree/Arnold shade
|
||||
// through scene lights only — without one they render solid black.
|
||||
// This distant light is the fallback when the HDR dome preset's texture
|
||||
// isn't bundled; ApplyLightPreset toggles between it and the dome.
|
||||
pxr::UsdLuxDistantLight light = pxr::UsdLuxDistantLight::Define(m_previewStage, kDistantLightPath);
|
||||
// pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(15000.0f));
|
||||
pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(1500.0f));
|
||||
// Default distant-light orientation shines along local -Z, which lights
|
||||
// the far side of the sphere from the camera's default view. Angle it
|
||||
// like a classic front-upper-left key light instead. Adding this scene
|
||||
// light suppresses UsdSceneRenderer's GL headlight fallback (it only
|
||||
// kicks in when the stage has no authored lights), so this needs to
|
||||
// carry the whole preview on its own.
|
||||
pxr::UsdGeomXformCommonAPI(light.GetPrim()).SetRotate(pxr::GfVec3f(-35.0f, -35.0f, 0.0f));
|
||||
|
||||
m_camera.SetStage(m_previewStage);
|
||||
m_renderer.SetStage(m_previewStage);
|
||||
m_renderer.SetShowGrid(false);
|
||||
// Raised refinement so the implicit sphere tessellates smoothly instead
|
||||
// of showing faceted silhouettes at swatch scale.
|
||||
m_renderer.SetComplexity(1.3f);
|
||||
|
||||
ApplyLightPreset(m_lightPreset);
|
||||
ApplyPreviewShape(m_previewShape); // activates one shape + frames the camera
|
||||
|
||||
// Maya-style default 3/4 view: orbit the framed camera ~45° around and
|
||||
// ~25° down instead of the dead-on front view (subsequent shape switches
|
||||
// keep whatever orientation the user has orbited to).
|
||||
m_camera.Tumble(-45.0, 25.0);
|
||||
m_renderer.SetCameraStateFromGfCamera(
|
||||
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
|
||||
|
||||
m_initialized = true;
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
|
||||
const pxr::SdfPath& materialPath,
|
||||
size_t graphRevision,
|
||||
const pxr::SdfPath& previewNodePath,
|
||||
const std::string& previewNodeOutput,
|
||||
bool previewOutputIsTerminal) {
|
||||
EnsureInitialized();
|
||||
|
||||
if (!sourceStage || materialPath.IsEmpty()) return;
|
||||
if (materialPath == m_materialPath && graphRevision == m_lastGraphRevision &&
|
||||
previewNodePath == m_previewNodePath && previewNodeOutput == m_previewNodeOutput)
|
||||
return; // nothing the shader ball cares about has changed
|
||||
|
||||
if (!sourceStage->GetPrimAtPath(materialPath)) {
|
||||
LOG_WARNING("MaterialPreviewRenderer: no prim at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Compose the material into the preview stage via a reference to the
|
||||
// source stage's root layer, rather than SdfCopySpec from one local
|
||||
// layer: a material pulled in by a reference (e.g. a .mtlx asset) has no
|
||||
// spec in any local layer — it only exists composed — and even local
|
||||
// materials can be spread across sublayers. Referencing at the identical
|
||||
// path keeps internal connection paths valid without remapping, and
|
||||
// relative asset paths keep resolving against their original layers.
|
||||
// Authored only when the material changes: the shared in-memory layers
|
||||
// recompose live, so revision bumps need no re-referencing.
|
||||
if (materialPath != m_materialPath) {
|
||||
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath);
|
||||
if (!mirrorPrim) {
|
||||
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
mirrorPrim.GetReferences().ClearReferences();
|
||||
mirrorPrim.GetReferences().AddReference(
|
||||
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
|
||||
}
|
||||
|
||||
pxr::UsdPrim shapesRoot = m_previewStage->GetPrimAtPath(kShapesRootPath);
|
||||
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
|
||||
if (!material) {
|
||||
LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
if (shapesRoot) // bound on the scope: inherits to whichever shape is active
|
||||
pxr::UsdShadeMaterialBindingAPI::Apply(shapesRoot).Bind(material);
|
||||
|
||||
// ── Selected-node preview (Hypershade-style) ─────────────────────────
|
||||
// All node-preview overrides (the scratch wrapper shader and the local
|
||||
// opinions on the material's surface outputs) live exclusively in the
|
||||
// preview stage's session layer, which is stronger than the root layer's
|
||||
// reference. Wiping them is then just clearing that layer — no per-spec
|
||||
// surgery (RemoveNameChild/RemoveProperty) that could interact badly with
|
||||
// Sdf's inert-spec cleanup, and no stale overrides on other materials.
|
||||
pxr::SdfLayerHandle sessionLayer = m_previewStage->GetSessionLayer();
|
||||
sessionLayer->Clear();
|
||||
|
||||
// Route the material's surface through the selected node. Nodes outside
|
||||
// the material's subtree (possible in hand-authored cross-scope networks)
|
||||
// aren't composed into the scratch stage, so those keep the whole-material
|
||||
// preview.
|
||||
if (!previewNodePath.IsEmpty() && !previewNodeOutput.empty() &&
|
||||
previewNodePath.HasPrefix(materialPath)) {
|
||||
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
|
||||
if (nodeShader) {
|
||||
pxr::UsdEditContext sessionEdit(m_previewStage,
|
||||
pxr::UsdEditTarget(sessionLayer));
|
||||
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
|
||||
pxr::TfToken sourceName(previewNodeOutput);
|
||||
if (!previewOutputIsTerminal) {
|
||||
// Pattern/texture output: show it as the diffuse color of a
|
||||
// scratch UsdPreviewSurface, like Maya's swatch for textures.
|
||||
pxr::UsdShadeShader wrap = pxr::UsdShadeShader::Define(
|
||||
m_previewStage, materialPath.AppendChild(kNodePreviewShaderName));
|
||||
wrap.CreateIdAttr(pxr::VtValue(pxr::TfToken("UsdPreviewSurface")));
|
||||
wrap.CreateInput(pxr::TfToken("roughness"), pxr::SdfValueTypeNames->Float)
|
||||
.Set(0.4f);
|
||||
wrap.CreateInput(pxr::TfToken("diffuseColor"), pxr::SdfValueTypeNames->Color3f)
|
||||
.ConnectToSource(sourceApi, sourceName);
|
||||
sourceApi = wrap.ConnectableAPI();
|
||||
sourceName = pxr::TfToken("surface");
|
||||
}
|
||||
// Override both the universal and the mtlx render-context outputs
|
||||
// so the preview target wins regardless of which context the
|
||||
// material authored (a .mtlx import only authors outputs:mtlx:*,
|
||||
// which Storm would otherwise prefer).
|
||||
material.CreateSurfaceOutput().ConnectToSource(sourceApi, sourceName);
|
||||
material.CreateSurfaceOutput(pxr::TfToken("mtlx")).ConnectToSource(sourceApi, sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
m_materialPath = materialPath;
|
||||
m_lastGraphRevision = graphRevision;
|
||||
m_previewNodePath = previewNodePath;
|
||||
m_previewNodeOutput = previewNodeOutput;
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::ApplyLightPreset(int index) {
|
||||
if (index < 0 || index >= kLightPresetCount) index = 0;
|
||||
m_lightPreset = index;
|
||||
|
||||
const std::string file = ResourcePath(std::string("resources/hdri/") + kLightPresets[index].file);
|
||||
const bool haveHdr = std::filesystem::exists(file);
|
||||
|
||||
pxr::UsdPrim distant = m_previewStage->GetPrimAtPath(kDistantLightPath);
|
||||
if (haveHdr) {
|
||||
pxr::UsdLuxDomeLight dome = pxr::UsdLuxDomeLight::Define(m_previewStage, kDomeLightPath);
|
||||
dome.CreateTextureFileAttr().Set(pxr::SdfAssetPath(file));
|
||||
// Declare the projection explicitly: left at "automatic", hdArnold
|
||||
// falls through to Arnold's skydome default ("angular" fisheye),
|
||||
// which squeezes an equirect panorama into a blob at the center.
|
||||
// Storm always samples latlong regardless, so this only affects
|
||||
// spec-following delegates.
|
||||
dome.CreateTextureFormatAttr().Set(pxr::UsdLuxTokens->latlong);
|
||||
dome.GetPrim().SetActive(true);
|
||||
if (distant) distant.SetActive(false); // dome carries the lighting alone
|
||||
UpdateDomeOrientation();
|
||||
} else {
|
||||
LOG_WARNING("MaterialPreviewRenderer: HDRI not found (" + file +
|
||||
") — falling back to distant light. Re-run CMake configure to download presets.");
|
||||
if (pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath))
|
||||
dome.SetActive(false);
|
||||
if (distant) distant.SetActive(true);
|
||||
}
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::ApplyPreviewShape(int index) {
|
||||
if (index < 0 || index >= kShapePresetCount) index = 0;
|
||||
// Fall back to the sphere if the requested shape's asset wasn't bundled.
|
||||
if (!m_previewStage->GetPrimAtPath(
|
||||
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[index].primName))))
|
||||
index = 0;
|
||||
m_previewShape = index;
|
||||
|
||||
for (int i = 0; i < kShapePresetCount; ++i) {
|
||||
pxr::UsdPrim prim = m_previewStage->GetPrimAtPath(
|
||||
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName)));
|
||||
if (prim)
|
||||
prim.SetActive(i == index);
|
||||
}
|
||||
|
||||
// Shapes differ wildly in size (the cloud VDB spans hundreds of units) —
|
||||
// reframe on the active shape's bounds; inactive prims don't contribute.
|
||||
m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds());
|
||||
m_camera.FrameSelection(m_previewBBox, kShapePresets[index].frameFit);
|
||||
m_renderer.SetCameraStateFromGfCamera(
|
||||
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::RenderShapeDropdown(float width) {
|
||||
if (!m_initialized) return;
|
||||
ImGui::SetNextItemWidth(width);
|
||||
if (ImGui::BeginCombo("##PreviewShape", kShapePresets[m_previewShape].label)) {
|
||||
for (int i = 0; i < kShapePresetCount; ++i) {
|
||||
if (!m_previewStage->GetPrimAtPath(
|
||||
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName))))
|
||||
continue; // asset not bundled
|
||||
if (ImGui::Selectable(kShapePresets[i].label, i == m_previewShape) && i != m_previewShape)
|
||||
ApplyPreviewShape(i);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Shader-ball preview geometry");
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::UpdateDomeOrientation() {
|
||||
if (!m_previewStage) return;
|
||||
pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath);
|
||||
if (!dome) return;
|
||||
|
||||
// Storm samples the dome texture with the pole along the light's local
|
||||
// +Y (domeLight.glslfx: v = acos(dir.y)/pi), while spec-following
|
||||
// delegates map it with the pole along local +Z (hdCycles feeds the
|
||||
// dome-local direction to Cycles' Z-up EnvironmentTextureNode; hdArnold's
|
||||
// skydome follows the same UsdLux convention). Rotating the dome -90°
|
||||
// about X for those points their +Z pole at world +Y, which reproduces
|
||||
// Storm's orientation exactly — including the horizontal (longitude)
|
||||
// alignment, since both parameterize u from the same axes.
|
||||
const pxr::TfToken rendererId = m_renderer.GetCurrentRendererId();
|
||||
const bool yUpPole = rendererId.IsEmpty() ||
|
||||
rendererId == pxr::TfToken("HdStormRendererPlugin");
|
||||
if (rendererId == pxr::TfToken("HdArnoldRendererPlugin")) {
|
||||
// With texture:format explicitly latlong, hdArnold aligns its skydome
|
||||
// with Storm on its own — earlier rotation offsets here were chasing
|
||||
// what turned out to be the angular-projection bug (format left
|
||||
// "automatic" fell through to Arnold's fisheye default). If a real
|
||||
// residual yaw shows up against Storm, put it in the Y component.
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
if(rendererId == pxr::TfToken("HdCyclesPlugin")) {
|
||||
// hdCycles' latlong mapping is flipped 180° from Storm's, so add
|
||||
// a yaw to match Storm's orientation. The pole is still along +Z.
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 270.0f, 0.0f));
|
||||
} else {
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 0.0f, 0.0f));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::RenderLightingDropdown(float width) {
|
||||
if (!m_initialized) return;
|
||||
ImGui::SetNextItemWidth(width);
|
||||
if (ImGui::BeginCombo("##PreviewLighting", kLightPresets[m_lightPreset].label)) {
|
||||
for (int i = 0; i < kLightPresetCount; ++i) {
|
||||
if (ImGui::Selectable(kLightPresets[i].label, i == m_lightPreset) && i != m_lightPreset)
|
||||
ApplyLightPreset(i);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Shader-ball HDR environment");
|
||||
}
|
||||
|
||||
uint32_t MaterialPreviewRenderer::Render(int width, int height) {
|
||||
EnsureInitialized();
|
||||
if (width <= 0 || height <= 0) return 0;
|
||||
|
||||
// Re-render while a progressive delegate (Arnold/Cycles/Embree) is still
|
||||
// accumulating — rendering only once would freeze its first noisy sample
|
||||
// pass into the cached texture. Storm converges in one pass, so the
|
||||
// dirty-flag caching still applies there.
|
||||
if (m_dirty || width != m_lastWidth || height != m_lastHeight || !m_renderer.IsConverged()) {
|
||||
// 2x supersample: the texture is displayed at (width, height) by
|
||||
// ImGui, so rendering larger gives the swatch cheap anti-aliasing.
|
||||
m_renderer.Render(width * 2, height * 2);
|
||||
m_dirty = false;
|
||||
m_lastWidth = width;
|
||||
m_lastHeight = height;
|
||||
}
|
||||
return m_renderer.GetColorTextureID();
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) {
|
||||
if (!m_initialized) return;
|
||||
m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity
|
||||
m_renderer.SetCameraStateFromGfCamera(
|
||||
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook) {
|
||||
m_renderer.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(ccMode));
|
||||
m_renderer.SetOcioDisplay(ocioDisplay);
|
||||
m_renderer.SetOcioView(ocioView);
|
||||
m_renderer.SetOcioColorSpace(ocioColorSpace);
|
||||
m_renderer.SetOcioLook(ocioLook);
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
|
||||
if (pluginId == m_renderer.GetCurrentRendererId()) return;
|
||||
m_renderer.SetRendererPlugin(pluginId);
|
||||
UpdateDomeOrientation(); // pole convention differs per delegate
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
pxr::TfToken MaterialPreviewRenderer::GetRendererPlugin() const {
|
||||
return m_renderer.GetCurrentRendererId();
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
|
||||
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
|
||||
std::string displayName = currentId.IsEmpty()
|
||||
? "Renderer"
|
||||
: UsdSceneRenderer::GetRendererDisplayName(currentId);
|
||||
|
||||
ImGui::Button(displayName.c_str(), ImVec2(width, 0.0f));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Shader-ball render delegate");
|
||||
|
||||
if (ImGui::BeginPopupContextItem("MaterialPreviewRendererPopup", ImGuiPopupFlags_MouseButtonLeft)) {
|
||||
for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
|
||||
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
|
||||
if (name.empty()) name = pluginId.GetString();
|
||||
bool selected = (pluginId == currentId);
|
||||
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
|
||||
SetRendererPlugin(pluginId);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/UsdSceneRenderer.h"
|
||||
#include "../core/ViewportCamera.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/base/gf/bbox3d.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Shader-ball preview: mirrors the material currently open in the Material
|
||||
/// Editor onto a scratch sphere and renders it through an independent Hydra
|
||||
/// instance, with its own renderer-delegate switch (mirrors the viewport's
|
||||
/// dropdown so the swatch can preview Storm, Cycles, etc. independently of
|
||||
/// the main viewport's choice).
|
||||
class MaterialPreviewRenderer {
|
||||
public:
|
||||
/// Mirrors the material subtree at materialPath (composed via a reference
|
||||
/// to sourceStage's root layer) onto the internal scratch stage and binds
|
||||
/// it to the preview sphere. graphRevision is a cheap caller-computed
|
||||
/// hash of the current node/link graph + values — compared against the
|
||||
/// last call so unrelated frames don't pay for a re-render (real cost
|
||||
/// with a path-traced delegate).
|
||||
///
|
||||
/// previewNodePath (optional, Hypershade-style): the shader node selected
|
||||
/// in the graph; the ball then previews that node's output instead of the
|
||||
/// material's own surface. Terminal (token-typed) outputs are connected
|
||||
/// as the surface directly; pattern outputs are routed into the
|
||||
/// diffuseColor of a scratch UsdPreviewSurface. Pass an empty path to
|
||||
/// preview the whole material.
|
||||
void SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
|
||||
const pxr::SdfPath& materialPath,
|
||||
size_t graphRevision,
|
||||
const pxr::SdfPath& previewNodePath = pxr::SdfPath(),
|
||||
const std::string& previewNodeOutput = std::string(),
|
||||
bool previewOutputIsTerminal = false);
|
||||
|
||||
/// Renders only if something changed since the last call. Returns the GL
|
||||
/// color texture ID suitable for ImGui::Image().
|
||||
uint32_t Render(int width, int height);
|
||||
|
||||
/// Click-drag orbit input, in pixel deltas (mirrors Maya's swatch orbit).
|
||||
void OrbitDrag(float deltaX, float deltaY);
|
||||
|
||||
void SetColorCorrection(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook);
|
||||
|
||||
/// Switches the shader-ball's render delegate (what RenderRendererDropdown
|
||||
/// does on click). No-op if already this plugin. Exposed so other
|
||||
/// consumers of a material preview (e.g. NodeThumbnailCache's in-canvas
|
||||
/// thumbnails) can mirror this renderer's current choice.
|
||||
void SetRendererPlugin(const pxr::TfToken& pluginId);
|
||||
/// Current render delegate (empty until EnsureInitialized() has run).
|
||||
pxr::TfToken GetRendererPlugin() const;
|
||||
|
||||
/// True once the last Render() call's image is final. Progressive
|
||||
/// delegates (Arnold/Cycles/Embree) return false for many calls after a
|
||||
/// material/renderer change while they accumulate samples — callers that
|
||||
/// cache the rendered texture (e.g. NodeThumbnailCache) must keep
|
||||
/// re-rendering until this is true, or they'll cache an early noisy (or
|
||||
/// still-black) frame.
|
||||
bool IsConverged() const { return m_renderer.IsConverged(); }
|
||||
|
||||
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
|
||||
/// width: ImGui item width (-1 = fill available).
|
||||
void RenderRendererDropdown(float width = -1.0f);
|
||||
|
||||
/// HDR-environment picker (External / Room / Interior / Sunset). Presets
|
||||
/// map to bundled resources/hdri/*.exr dome-light textures; a preset
|
||||
/// whose file is missing falls back to the distant key light.
|
||||
/// width: ImGui item width (-1 = fill available).
|
||||
void RenderLightingDropdown(float width = -1.0f);
|
||||
|
||||
/// Preview-geometry picker (Sphere / Cube / Cylinder / Teapot / Hair /
|
||||
/// Cloud Volume). Shapes whose asset is missing are omitted.
|
||||
/// width: ImGui item width (-1 = fill available).
|
||||
void RenderShapeDropdown(float width = -1.0f);
|
||||
|
||||
private:
|
||||
void EnsureInitialized();
|
||||
void ApplyLightPreset(int index);
|
||||
/// Activates the chosen preview shape (deactivating the others) and
|
||||
/// reframes the camera to its bounds.
|
||||
void ApplyPreviewShape(int index);
|
||||
/// Compensates the dome-light pole convention per render delegate: Storm
|
||||
/// samples with the pole along local +Y, spec-following delegates
|
||||
/// (hdArnold, hdCycles) along local +Z. Called on preset and renderer
|
||||
/// switches so the HDR environment reads identically everywhere.
|
||||
void UpdateDomeOrientation();
|
||||
|
||||
pxr::UsdStageRefPtr m_previewStage;
|
||||
UsdSceneRenderer m_renderer;
|
||||
ViewportCamera m_camera;
|
||||
pxr::GfBBox3d m_previewBBox;
|
||||
|
||||
pxr::SdfPath m_materialPath; // path (identical in source & scratch stage)
|
||||
size_t m_lastGraphRevision = 0;
|
||||
pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material)
|
||||
std::string m_previewNodeOutput;
|
||||
int m_lightPreset = 0;
|
||||
int m_previewShape = 0;
|
||||
bool m_initialized = false;
|
||||
bool m_dirty = true;
|
||||
int m_lastWidth = 0;
|
||||
int m_lastHeight = 0;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,12 @@
|
||||
# pragma once
|
||||
#include "imgui.h"
|
||||
|
||||
namespace ax {
|
||||
namespace Drawing {
|
||||
|
||||
enum class IconType { Flow, Circle, Square, Grid, RoundSquare, Diamond };
|
||||
|
||||
void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor);
|
||||
|
||||
} // namespace Drawing
|
||||
} // namespace ax
|
||||
@@ -0,0 +1,642 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Math 2D
|
||||
//------------------------------------------------------------------------------
|
||||
# if !defined(__AX_MATH_2D_INL__)
|
||||
# define __AX_MATH_2D_INL__
|
||||
# pragma once
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# if defined(__cplusplus)
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include <utility>
|
||||
# include <cmath>
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
template <typename T, typename P>
|
||||
inline ax::basic_point<T> operator + (const ax::basic_size<P>& lhs, const ax::basic_point<T>& rhs)
|
||||
{
|
||||
return static_cast<ax::basic_point<T>>(lhs) + rhs;
|
||||
}
|
||||
|
||||
template <typename T, typename P>
|
||||
inline ax::basic_point<T> operator + (const ax::basic_point<T>& lhs, const ax::basic_size<P>& rhs)
|
||||
{
|
||||
return lhs + static_cast<ax::basic_point<T>>(rhs);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
template <typename T>
|
||||
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point(const point_t& p, bool on_edge, float radius) const
|
||||
{
|
||||
auto point = get_closest_point(p, on_edge);
|
||||
|
||||
const auto offset = p - point;
|
||||
const auto distance_sq = offset.x * offset.x + offset.y * offset.y;
|
||||
if (distance_sq <= 0)
|
||||
return point;
|
||||
|
||||
const auto distance = sqrtf(static_cast<float>(distance_sq));
|
||||
|
||||
return point + static_cast<basic_point<T>>(std::min(distance, static_cast<float>(radius)) * static_cast<basic_point<float>>(offset) * (1.0f / distance));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point(const basic_rect& r) const
|
||||
{
|
||||
point_t result;
|
||||
if (r.left() >= right())
|
||||
result.x = right();
|
||||
else if (r.right() <= left())
|
||||
result.x = left();
|
||||
else
|
||||
result.x = (std::max(left(), r.left()) + std::min(right(), r.right())) / 2;
|
||||
|
||||
if (r.top() >= bottom())
|
||||
result.y = bottom();
|
||||
else if (r.bottom() <= top())
|
||||
result.y = top();
|
||||
else
|
||||
result.y = (std::max(top(), r.top()) + std::min(bottom(), r.bottom())) / 2;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point_hollow(const point_t& p, T rounding, rect_region* region) const
|
||||
{
|
||||
rounding = std::min(rounding, std::min(w, h) / 2);
|
||||
|
||||
const auto inner = expanded(-rounding);
|
||||
|
||||
if (p.x <= inner.left() && p.y <= inner.top())
|
||||
{
|
||||
if (region) *region = rect_region::top_left;
|
||||
return inner.top_left().followed(p + point_t(-1, -1), rounding);
|
||||
}
|
||||
else if (p.x >= inner.right() && p.y <= inner.top())
|
||||
{
|
||||
if (region) *region = rect_region::top_right;
|
||||
return inner.top_right().followed(p + point_t(1, -1), rounding);
|
||||
}
|
||||
else if (p.x <= inner.left() && p.y >= inner.bottom())
|
||||
{
|
||||
if (region) *region = rect_region::bottom_left;
|
||||
return inner.bottom_left().followed(p + point_t(-1, 1), rounding);
|
||||
}
|
||||
else if (p.x >= inner.right() && p.y >= inner.bottom())
|
||||
{
|
||||
if (region) *region = rect_region::bottom_right;
|
||||
return inner.bottom_right().followed(p + point_t(1, 1), rounding);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (contains(p) || region)
|
||||
{
|
||||
const auto l_diff = abs(p.x - left());
|
||||
const auto r_diff = abs(p.x - right());
|
||||
const auto t_diff = abs(p.y - top());
|
||||
const auto b_diff = abs(p.y - bottom());
|
||||
|
||||
if (l_diff <= r_diff && l_diff <= t_diff && l_diff <= b_diff)
|
||||
{
|
||||
if (region) *region = rect_region::left;
|
||||
return point_t(left(), p.y);
|
||||
}
|
||||
else if (r_diff <= l_diff && r_diff <= t_diff && r_diff <= b_diff)
|
||||
{
|
||||
if (region) *region = rect_region::right;
|
||||
return point_t(right(), p.y);
|
||||
}
|
||||
else if (t_diff <= l_diff && t_diff <= r_diff && t_diff <= b_diff)
|
||||
{
|
||||
if (region) *region = rect_region::top;
|
||||
return point_t(p.x, top());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (region) *region = rect_region::bottom;
|
||||
return point_t(p.x, bottom());
|
||||
}
|
||||
}
|
||||
else
|
||||
return get_closest_point(p, true);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r) const
|
||||
{
|
||||
auto a = get_closest_point(r);
|
||||
auto b = r.get_closest_point(*this);
|
||||
|
||||
auto distribute = [](T& a, T& b, T a0, T a1, T b0, T b1)
|
||||
{
|
||||
if (a0 >= b1 || a1 <= b0)
|
||||
return;
|
||||
|
||||
const auto aw = a1 - a0;
|
||||
const auto bw = b1 - b0;
|
||||
|
||||
if (aw > bw)
|
||||
{
|
||||
b = b0 + bw - bw * (a - a0) / aw;
|
||||
a = b;
|
||||
}
|
||||
else if (aw < bw)
|
||||
{
|
||||
a = a0 + aw - aw * (b - b0) / bw;
|
||||
b = a;
|
||||
}
|
||||
};
|
||||
|
||||
distribute(a.x, b.x, left(), right(), r.left(), r.right());
|
||||
distribute(a.y, b.y, top(), bottom(), r.top(), r.bottom());
|
||||
|
||||
return basic_line<T>(a, b);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r, T radius) const
|
||||
{
|
||||
return get_closest_line(r, radius, radius);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r, T radius_a, T radius_b) const
|
||||
{
|
||||
auto line = get_closest_line(r);
|
||||
if (radius_a < 0)
|
||||
radius_a = 0;
|
||||
if (radius_b < 0)
|
||||
radius_b = 0;
|
||||
|
||||
if (radius_a == 0 && radius_b == 0)
|
||||
return line;
|
||||
|
||||
const auto offset = line.b - line.a;
|
||||
const auto length_sq = offset.x * offset.x + offset.y * offset.y;
|
||||
const auto radius_a_sq = radius_a * radius_a;
|
||||
const auto radius_b_sq = radius_b * radius_b;
|
||||
|
||||
if (length_sq <= 0)
|
||||
return line;
|
||||
|
||||
const auto length = sqrtf(static_cast<float>(length_sq));
|
||||
const auto direction = ax::basic_point<float>(offset.x / length, offset.y / length);
|
||||
|
||||
const auto total_radius_sq = radius_a_sq + radius_b_sq;
|
||||
float radius_a_f = static_cast<float>(radius_a);
|
||||
float radius_b_f = static_cast<float>(radius_b);
|
||||
if (total_radius_sq > length_sq)
|
||||
{
|
||||
const auto scale = length / (radius_a_f + radius_b_f);
|
||||
radius_a_f *= scale;
|
||||
radius_b_f *= scale;
|
||||
}
|
||||
|
||||
line.a = line.a + static_cast<point_t>(direction * radius_a_f);
|
||||
line.b = line.b - static_cast<point_t>(direction * radius_b_f);
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline void ax::matrix::zero()
|
||||
{
|
||||
*this = matrix(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
inline void ax::matrix::reset()
|
||||
{
|
||||
*this = matrix();
|
||||
}
|
||||
|
||||
inline bool ax::matrix::invert()
|
||||
{
|
||||
const float det = (m11 * m22 - m21 * m12);
|
||||
if (det == 0.0f)
|
||||
return false;
|
||||
|
||||
const float invDet = 1.0f / det;
|
||||
|
||||
*this = matrix(
|
||||
m22 * invDet,
|
||||
-m12 * invDet,
|
||||
-m21 * invDet,
|
||||
m11 * invDet,
|
||||
-(-m21 * m32 + m22 * m31) * invDet,
|
||||
-( m11 * m32 - m12 * m31) * invDet);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void ax::matrix::translate(float x, float y, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
combine(matrix(1.0f, 0.0f, 0.0f, 1.0f, x, y), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix::rotate(float angle, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
float angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const float c = cosf(angleRad);
|
||||
const float s = sinf(angleRad);
|
||||
|
||||
combine(matrix(c, s, -s, c, 0.0f, 0.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix::rotate_at(float angle, float cx, float cy, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
const auto angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const auto c = cosf(angleRad);
|
||||
const auto s = sinf(angleRad);
|
||||
|
||||
combine(matrix(c, s, -s, c,
|
||||
-cx * c - cy * -s + cx,
|
||||
-cx * s - cy * c + cy), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix::scale(float x, float y, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
combine(matrix(x, 0.0f, 0.0f, y, 0.0f, 0.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix::shear(float x, float y, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
combine(matrix(1.0f, y, x, 1.0f, 0.0f, 0.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix::combine(const matrix& matrix, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
if (order == matrix_order::set)
|
||||
{
|
||||
if (this != &matrix)
|
||||
*this = matrix;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* am = this;
|
||||
const auto* bm = &matrix;
|
||||
|
||||
if (order == matrix_order::append)
|
||||
{
|
||||
using std::swap;
|
||||
swap(am, bm);
|
||||
}
|
||||
|
||||
*this = ax::matrix(
|
||||
am->m11 * bm->m11 + am->m21 * bm->m12,
|
||||
bm->m11 * am->m12 + bm->m12 * am->m22,
|
||||
bm->m21 * am->m11 + bm->m22 * am->m21,
|
||||
am->m12 * bm->m21 + am->m22 * bm->m22,
|
||||
bm->m31 * am->m11 + bm->m32 * am->m21 + am->m31,
|
||||
bm->m31 * am->m12 + bm->m32 * am->m22 + am->m32);
|
||||
}
|
||||
|
||||
inline ax::matrix ax::matrix::inverted() const
|
||||
{
|
||||
matrix inverted = *this;
|
||||
inverted.invert();
|
||||
return inverted;
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline void ax::matrix4::zero()
|
||||
{
|
||||
*this = matrix4(
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::reset()
|
||||
{
|
||||
*this = matrix4();
|
||||
}
|
||||
|
||||
inline bool ax::matrix4::invert()
|
||||
{
|
||||
# if 0
|
||||
// wild magic 4 inverse - http://www.geometrictools.com/Documentation/LaplaceExpansionTheorem.pdf
|
||||
// 84 multiplications
|
||||
// 66 adds/subs
|
||||
// 1 division
|
||||
|
||||
// 24
|
||||
const float fA0 = m11 * m22 - m12 * m21;
|
||||
const float fA1 = m11 * m23 - m13 * m21;
|
||||
const float fA2 = m11 * m24 - m14 * m21;
|
||||
const float fA3 = m12 * m23 - m13 * m22;
|
||||
const float fA4 = m12 * m24 - m14 * m22;
|
||||
const float fA5 = m13 * m24 - m14 * m23;
|
||||
const float fB0 = m31 * m42 - m32 * m41;
|
||||
const float fB1 = m31 * m43 - m33 * m41;
|
||||
const float fB2 = m31 * m44 - m34 * m41;
|
||||
const float fB3 = m32 * m43 - m33 * m42;
|
||||
const float fB4 = m32 * m44 - m34 * m42;
|
||||
const float fB5 = m33 * m44 - m34 * m43;
|
||||
|
||||
// 6
|
||||
const float det = fA0 * fB5 - fA1 * fB4 + fA2 * fB3 + fA3 * fB2 - fA4 * fB1 + fA5 * fB0;
|
||||
|
||||
if (det == 0.0f)
|
||||
{
|
||||
zero();
|
||||
return false;
|
||||
}
|
||||
|
||||
const float invDet = 1.0f / det;
|
||||
|
||||
// 36 + 16
|
||||
*this = Matrix4(
|
||||
( m22 * fB5 - m23 * fB4 + m24 * fB3) * invDet,
|
||||
(-m12 * fB5 + m13 * fB4 - m14 * fB3) * invDet,
|
||||
( m42 * fA5 - m43 * fA4 + m44 * fA3) * invDet,
|
||||
(-m32 * fA5 + m33 * fA4 - m34 * fA3) * invDet,
|
||||
(-m21 * fB5 + m23 * fB2 - m24 * fB1) * invDet,
|
||||
( m11 * fB5 - m13 * fB2 + m14 * fB1) * invDet,
|
||||
(-m41 * fA5 + m43 * fA2 - m44 * fA1) * invDet,
|
||||
( m31 * fA5 - m33 * fA2 + m34 * fA1) * invDet,
|
||||
( m21 * fB4 - m22 * fB2 + m24 * fB0) * invDet,
|
||||
(-m11 * fB4 + m12 * fB2 - m14 * fB0) * invDet,
|
||||
( m41 * fA4 - m42 * fA2 + m44 * fA0) * invDet,
|
||||
(-m31 * fA4 + m32 * fA2 - m34 * fA0) * invDet,
|
||||
(-m21 * fB3 + m22 * fB1 - m23 * fB0) * invDet,
|
||||
( m11 * fB3 - m12 * fB1 + m13 * fB0) * invDet,
|
||||
(-m41 * fA3 + m42 * fA1 - m43 * fA0) * invDet,
|
||||
( m31 * fA3 - m32 * fA1 + m33 * fA0) * invDet);
|
||||
|
||||
return true;
|
||||
# else
|
||||
// 214 multiplications
|
||||
// 80 adds/subs
|
||||
// 1 division
|
||||
const float d00 = m22 * m33 * m44 + m32 * m43 * m24 + m42 * m23 * m34 - m24 * m33 * m42 - m34 * m43 * m22 - m44 * m23 * m32;
|
||||
const float d01 = m12 * m33 * m44 + m32 * m43 * m14 + m42 * m13 * m34 - m14 * m33 * m42 - m34 * m43 * m12 - m44 * m13 * m32;
|
||||
const float d02 = m12 * m23 * m44 + m22 * m43 * m14 + m42 * m13 * m24 - m14 * m23 * m42 - m24 * m43 * m12 - m44 * m13 * m22;
|
||||
const float d03 = m12 * m23 * m34 + m22 * m33 * m14 + m32 * m13 * m24 - m14 * m23 * m32 - m24 * m33 * m12 - m34 * m13 * m22;
|
||||
|
||||
const float d10 = m21 * m33 * m44 + m31 * m43 * m24 + m41 * m23 * m34 - m24 * m33 * m41 - m34 * m43 * m21 - m44 * m23 * m31;
|
||||
const float d11 = m11 * m33 * m44 + m31 * m43 * m14 + m41 * m13 * m34 - m14 * m33 * m41 - m34 * m43 * m11 - m44 * m13 * m31;
|
||||
const float d12 = m11 * m23 * m44 + m21 * m43 * m14 + m41 * m13 * m24 - m14 * m23 * m41 - m24 * m43 * m11 - m44 * m13 * m21;
|
||||
const float d13 = m11 * m23 * m34 + m21 * m33 * m14 + m31 * m13 * m24 - m14 * m23 * m31 - m24 * m33 * m11 - m34 * m13 * m21;
|
||||
|
||||
const float d20 = m21 * m32 * m44 + m31 * m42 * m24 + m41 * m22 * m34 - m24 * m32 * m41 - m34 * m42 * m21 - m44 * m22 * m31;
|
||||
const float d21 = m11 * m32 * m44 + m31 * m42 * m14 + m41 * m12 * m34 - m14 * m32 * m41 - m34 * m42 * m11 - m44 * m12 * m31;
|
||||
const float d22 = m11 * m22 * m44 + m21 * m42 * m14 + m41 * m12 * m24 - m14 * m22 * m41 - m24 * m42 * m11 - m44 * m12 * m21;
|
||||
const float d23 = m11 * m22 * m34 + m21 * m32 * m14 + m31 * m12 * m24 - m14 * m22 * m31 - m24 * m32 * m11 - m34 * m12 * m21;
|
||||
|
||||
const float d30 = m21 * m32 * m43 + m31 * m42 * m23 + m41 * m22 * m33 - m23 * m32 * m41 - m33 * m42 * m21 - m43 * m22 * m31;
|
||||
const float d31 = m11 * m32 * m43 + m31 * m42 * m13 + m41 * m12 * m33 - m13 * m32 * m41 - m33 * m42 * m11 - m43 * m12 * m31;
|
||||
const float d32 = m11 * m22 * m43 + m21 * m42 * m13 + m41 * m12 * m23 - m13 * m22 * m41 - m23 * m42 * m11 - m43 * m12 * m21;
|
||||
const float d33 = m11 * m22 * m33 + m21 * m32 * m13 + m31 * m12 * m23 - m13 * m22 * m31 - m23 * m32 * m11 - m33 * m12 * m21;
|
||||
|
||||
const float det = m11 * d00 - m21 * d01 + m31 * d02 - m41 * d03;
|
||||
|
||||
if (det == 0.0f)
|
||||
{
|
||||
zero();
|
||||
return false;
|
||||
}
|
||||
|
||||
const float invDet = 1.0f / det;
|
||||
|
||||
m11 = d00 * invDet; m21 = -d10 * invDet; m31 = d20 * invDet; m41 = -d30 * invDet;
|
||||
m12 = -d01 * invDet; m22 = d11 * invDet; m32 = -d21 * invDet; m42 = d31 * invDet;
|
||||
m13 = d02 * invDet; m23 = -d12 * invDet; m33 = d22 * invDet; m43 = -d32 * invDet;
|
||||
m14 = -d03 * invDet; m24 = d13 * invDet; m34 = -d23 * invDet; m44 = d33 * invDet;
|
||||
|
||||
return true;
|
||||
# endif
|
||||
}
|
||||
|
||||
inline void ax::matrix4::transpose()
|
||||
{
|
||||
using std::swap;
|
||||
|
||||
swap(m12, m21);
|
||||
swap(m13, m31);
|
||||
swap(m14, m41);
|
||||
swap(m23, m32);
|
||||
swap(m24, m42);
|
||||
swap(m34, m43);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::translate(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
combine(matrix4(
|
||||
1.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 0.0f,
|
||||
x, y, z, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::rotate_x(float angle, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
const auto angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const auto c = cosf(angleRad);
|
||||
const auto s = sinf(angleRad);
|
||||
|
||||
combine(matrix4(
|
||||
1.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, c, -s, 0.0f,
|
||||
0.0f, s, c, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::rotate_y(float angle, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
const auto angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const auto c = cosf(angleRad);
|
||||
const auto s = sinf(angleRad);
|
||||
|
||||
combine(matrix4(
|
||||
c, 0.0f, -s, 0.0f,
|
||||
0.0f, 1.0f, 0.0f, 0.0f,
|
||||
s, 0.0f, c, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::rotate_z(float angle, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
const auto angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const auto c = cosf(angleRad);
|
||||
const auto s = sinf(angleRad);
|
||||
|
||||
combine(matrix4(
|
||||
c, -s, 0.0f, 0.0f,
|
||||
s, c, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::rotate_axis(float angle, float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
const auto angleRad = angle * AX_PI / 180.0f;
|
||||
|
||||
const auto c = cosf(angleRad);
|
||||
const auto s = sinf(angleRad);
|
||||
|
||||
const auto rc = 1.0f - c;
|
||||
|
||||
combine(matrix4(
|
||||
(rc * x * x) + c, (rc * x * y) + (z * s), (rc * x * z) - (y * s), 0.0f,
|
||||
(rc * x * y) - (z * s), (rc * y * y) + c, (rc * z * y) + (x * s), 0.0f,
|
||||
(rc * x * z) + (y * s), (rc * y * z) - (x * s), (rc * z * z) + c, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::scale(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
combine(matrix4(
|
||||
x, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, y, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, z, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f), order);
|
||||
}
|
||||
|
||||
inline void ax::matrix4::combine(const matrix4& matrix, matrix_order order/* = matrix_order::prepend*/)
|
||||
{
|
||||
if (order == matrix_order::set)
|
||||
{
|
||||
if (this != &matrix)
|
||||
*this = matrix;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* am = this;
|
||||
const auto* bm = &matrix;
|
||||
|
||||
if (order == matrix_order::append)
|
||||
{
|
||||
using std::swap;
|
||||
swap(am, bm);
|
||||
}
|
||||
|
||||
*this = matrix4(
|
||||
am->m11 * bm->m11 + am->m21 * bm->m12 + am->m31 * bm->m13 + am->m41 * bm->m14,
|
||||
am->m12 * bm->m11 + am->m22 * bm->m12 + am->m32 * bm->m13 + am->m42 * bm->m14,
|
||||
am->m13 * bm->m11 + am->m23 * bm->m12 + am->m33 * bm->m13 + am->m43 * bm->m14,
|
||||
am->m14 * bm->m11 + am->m24 * bm->m12 + am->m34 * bm->m13 + am->m44 * bm->m14,
|
||||
|
||||
am->m11 * bm->m21 + am->m21 * bm->m22 + am->m31 * bm->m23 + am->m41 * bm->m24,
|
||||
am->m12 * bm->m21 + am->m22 * bm->m22 + am->m32 * bm->m23 + am->m42 * bm->m24,
|
||||
am->m13 * bm->m21 + am->m23 * bm->m22 + am->m33 * bm->m23 + am->m43 * bm->m24,
|
||||
am->m14 * bm->m21 + am->m24 * bm->m22 + am->m34 * bm->m23 + am->m44 * bm->m24,
|
||||
|
||||
am->m11 * bm->m31 + am->m21 * bm->m32 + am->m31 * bm->m33 + am->m41 * bm->m34,
|
||||
am->m12 * bm->m31 + am->m22 * bm->m32 + am->m32 * bm->m33 + am->m42 * bm->m34,
|
||||
am->m13 * bm->m31 + am->m23 * bm->m32 + am->m33 * bm->m33 + am->m43 * bm->m34,
|
||||
am->m14 * bm->m31 + am->m24 * bm->m32 + am->m34 * bm->m33 + am->m44 * bm->m34,
|
||||
|
||||
am->m11 * bm->m41 + am->m21 * bm->m42 + am->m31 * bm->m43 + am->m41 * bm->m44,
|
||||
am->m12 * bm->m41 + am->m22 * bm->m42 + am->m32 * bm->m43 + am->m42 * bm->m44,
|
||||
am->m13 * bm->m41 + am->m23 * bm->m42 + am->m33 * bm->m43 + am->m43 * bm->m44,
|
||||
am->m14 * bm->m41 + am->m24 * bm->m42 + am->m34 * bm->m43 + am->m44 * bm->m44);
|
||||
}
|
||||
|
||||
inline ax::matrix4 ax::matrix4::inverted() const
|
||||
{
|
||||
matrix4 inverted = *this;
|
||||
inverted.invert();
|
||||
return inverted;
|
||||
}
|
||||
|
||||
inline ax::matrix4 ax::matrix4::transposed() const
|
||||
{
|
||||
matrix4 transposed = *this;
|
||||
transposed.transpose();
|
||||
return transposed;
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
namespace ax {
|
||||
namespace detail {
|
||||
|
||||
# if defined(__clang__) || defined(__GNUC__)
|
||||
template <typename M, typename T>
|
||||
void transform_points(const M& m, basic_point<T>* points, size_t count);
|
||||
# else
|
||||
template <typename M, typename T>
|
||||
inline void transform_points(const M& m, basic_point<T>* points, size_t count)
|
||||
{
|
||||
// UsdLayerManager local patch: static_assert(false, ...) here is
|
||||
// ill-formed regardless of instantiation (its condition doesn't depend
|
||||
// on a template parameter) — stricter MSVC (/permissive-, set for MSVC
|
||||
// >= 1910 in this project's CMakeLists.txt) diagnoses it just from this
|
||||
// primary template's declaration, even when every real call resolves to
|
||||
// one of the matrix/matrix4 specializations below and this fallback is
|
||||
// never instantiated. Make the condition depend on T so it only fires
|
||||
// if something actually instantiates this unspecialized fallback.
|
||||
static_assert(sizeof(T) == 0, "This combination of matrix type and point type is not supported");
|
||||
}
|
||||
# endif
|
||||
|
||||
template <typename M, typename T>
|
||||
inline void transform_vectors(const M& m, basic_point<T>* points, size_t count)
|
||||
{
|
||||
typedef basic_point<T> point_t;
|
||||
|
||||
for (size_t i = 0; i < count; ++i, ++points)
|
||||
{
|
||||
auto x = m.m11 * points->x + m.m21 * points->y;
|
||||
auto y = m.m12 * points->x + m.m22 * points->y;
|
||||
|
||||
points->x = static_cast<typename point_t::value_type>(x);
|
||||
points->y = static_cast<typename point_t::value_type>(y);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline void transform_points(const matrix& m, basic_point<T>* points, size_t count)
|
||||
{
|
||||
typedef basic_point<T> point_t;
|
||||
|
||||
for (size_t i = 0; i < count; ++i, ++points)
|
||||
{
|
||||
auto x = m.m11 * points->x + m.m21 * points->y + m.m31;
|
||||
auto y = m.m12 * points->x + m.m22 * points->y + m.m32;
|
||||
|
||||
points->x = static_cast<typename point_t::value_type>(x);
|
||||
points->y = static_cast<typename point_t::value_type>(y);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline void transform_points(const matrix4& m, basic_point<T>* points, size_t count)
|
||||
{
|
||||
typedef basic_point<T> point_t;
|
||||
|
||||
for (size_t i = 0; i < count; ++i, ++points)
|
||||
{
|
||||
auto x = m.m11 * points->x + m.m21 * points->y + m.m41;
|
||||
auto y = m.m12 * points->x + m.m22 * points->y + m.m42;
|
||||
|
||||
points->x = static_cast<typename point_t::value_type>(x);
|
||||
points->y = static_cast<typename point_t::value_type>(y);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
} // namespace ax
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // defined(__cplusplus)
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // __AX_MATH_2D_INL__
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "Drawing.h"
|
||||
|
||||
namespace ax {
|
||||
namespace Widgets {
|
||||
|
||||
using Drawing::IconType;
|
||||
|
||||
void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0));
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace ax
|
||||
@@ -0,0 +1,69 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
//------------------------------------------------------------------------------
|
||||
# pragma once
|
||||
# include "imgui.h"
|
||||
# include "ax/Math2D.h"
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//namespace ax {
|
||||
//namespace ImGuiInterop {
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Modern ImGui (1.89.4+) already defines these operators in imgui.h itself
|
||||
// when IMGUI_DEFINE_MATH_OPERATORS is set before its first include — guard
|
||||
// against redefinition against this project's ImGui version.
|
||||
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
|
||||
static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; }
|
||||
static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; }
|
||||
static inline ImVec2 operator+(const ImVec2& lhs) { return ImVec2( lhs.x, lhs.y); }
|
||||
static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); }
|
||||
static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y); }
|
||||
static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y); }
|
||||
static inline ImVec2 operator*(const ImVec2& lhs, float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); }
|
||||
static inline ImVec2 operator*(float lhs, const ImVec2& rhs) { return ImVec2(lhs * rhs.x, lhs * rhs.y); }
|
||||
# endif
|
||||
|
||||
static inline int roundi(float value) { return static_cast<int>(value); }
|
||||
static inline ax::point to_point(const ImVec2& value) { return ax::point(roundi(value.x), roundi(value.y)); }
|
||||
static inline ax::pointf to_pointf(const ImVec2& value) { return ax::pointf(value.x, value.y); }
|
||||
static inline ax::pointf to_pointf(const ax::point& value) { return ax::pointf(static_cast<float>(value.x), static_cast<float>(value.y)); }
|
||||
static inline ax::size to_size (const ImVec2& value) { return ax::size (roundi(value.x), roundi(value.y)); }
|
||||
static inline ax::sizef to_sizef(const ImVec2& value) { return ax::sizef(value.x, value.y); }
|
||||
static inline ImVec2 to_imvec(const ax::point& value) { return ImVec2(static_cast<float>(value.x), static_cast<float>(value.y)); }
|
||||
static inline ImVec2 to_imvec(const ax::pointf& value) { return ImVec2(value.x, value.y); }
|
||||
static inline ImVec2 to_imvec(const ax::size& value) { return ImVec2(static_cast<float>(value.w), static_cast<float>(value.h)); }
|
||||
static inline ImVec2 to_imvec(const ax::sizef& value) { return ImVec2(value.w, value.h); }
|
||||
static inline ax::rect ImGui_GetItemRect() { return ax::rect(to_point(ImGui::GetItemRectMin()), to_point(ImGui::GetItemRectMax())); }
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
struct FringeScaleScope
|
||||
{
|
||||
FringeScaleScope(float scale)
|
||||
: m_LastFringeScale(ImGui::GetWindowDrawList()->_FringeScale)
|
||||
{
|
||||
ImGui::GetWindowDrawList()->_FringeScale = scale;
|
||||
}
|
||||
|
||||
~FringeScaleScope()
|
||||
{
|
||||
ImGui::GetWindowDrawList()->_FringeScale = m_LastFringeScale;
|
||||
}
|
||||
|
||||
private:
|
||||
float m_LastFringeScale;
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//} // namespace ImGuiInterop
|
||||
//} // namespace ax
|
||||
@@ -0,0 +1,207 @@
|
||||
//# include "stdafx.h"
|
||||
# include "ax/Drawing.h"
|
||||
# include "ax/Math2D.h"
|
||||
# include "Interop.h"
|
||||
# include <cmath>
|
||||
|
||||
void ax::Drawing::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor)
|
||||
{
|
||||
auto rect = ax::rect(to_point(a), to_point(b));
|
||||
const auto outline_scale = rect.w / 24.0f;
|
||||
const auto extra_segments = roundi(2 * outline_scale); // for full circle
|
||||
|
||||
if (type == IconType::Flow)
|
||||
{
|
||||
const auto origin_scale = rect.w / 24.0f;
|
||||
|
||||
const auto offset_x = 1.0f * origin_scale;
|
||||
const auto offset_y = 0.0f * origin_scale;
|
||||
const auto margin = (filled ? 2.0f : 2.0f) * origin_scale;
|
||||
const auto rounding = 0.1f * origin_scale;
|
||||
const auto tip_round = 0.7f; // percentage of triangle edge (for tip)
|
||||
//const auto edge_round = 0.7f; // percentage of triangle edge (for corner)
|
||||
const auto canvas = rectf(
|
||||
rect.x + margin + offset_x,
|
||||
rect.y + margin + offset_y,
|
||||
rect.w - margin * 2.0f,
|
||||
rect.h - margin * 2.0f);
|
||||
|
||||
const auto left = canvas.x + canvas.w * 0.5f * 0.3f;
|
||||
const auto right = canvas.x + canvas.w - canvas.w * 0.5f * 0.3f;
|
||||
const auto top = canvas.y + canvas.h * 0.5f * 0.2f;
|
||||
const auto bottom = canvas.y + canvas.h - canvas.h * 0.5f * 0.2f;
|
||||
const auto center_y = (top + bottom) * 0.5f;
|
||||
//const auto angle = AX_PI * 0.5f * 0.5f * 0.5f;
|
||||
|
||||
const auto tip_top = ImVec2(canvas.x + canvas.w * 0.5f, top);
|
||||
const auto tip_right = ImVec2(right, center_y);
|
||||
const auto tip_bottom = ImVec2(canvas.x + canvas.w * 0.5f, bottom);
|
||||
|
||||
drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding));
|
||||
drawList->PathBezierCubicCurveTo(
|
||||
ImVec2(left, top),
|
||||
ImVec2(left, top),
|
||||
ImVec2(left, top) + ImVec2(rounding, 0));
|
||||
drawList->PathLineTo(tip_top);
|
||||
drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round);
|
||||
drawList->PathBezierCubicCurveTo(
|
||||
tip_right,
|
||||
tip_right,
|
||||
tip_bottom + (tip_right - tip_bottom) * tip_round);
|
||||
drawList->PathLineTo(tip_bottom);
|
||||
drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0));
|
||||
drawList->PathBezierCubicCurveTo(
|
||||
ImVec2(left, bottom),
|
||||
ImVec2(left, bottom),
|
||||
ImVec2(left, bottom) - ImVec2(0, rounding));
|
||||
|
||||
if (!filled)
|
||||
{
|
||||
if (innerColor & 0xFF000000)
|
||||
drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
|
||||
|
||||
drawList->PathStroke(color, true, 2.0f * outline_scale);
|
||||
}
|
||||
else
|
||||
drawList->PathFillConvex(color);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto triangleStart = rect.center_x() + 0.32f * rect.w;
|
||||
|
||||
rect.x -= roundi(rect.w * 0.25f * 0.25f);
|
||||
|
||||
if (type == IconType::Circle)
|
||||
{
|
||||
const auto c = to_imvec(rect.center());
|
||||
|
||||
if (!filled)
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
|
||||
|
||||
if (innerColor & 0xFF000000)
|
||||
drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments);
|
||||
drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale);
|
||||
}
|
||||
else
|
||||
drawList->AddCircleFilled(c, 0.5f * rect.w / 2.0f, color, 12 + extra_segments);
|
||||
}
|
||||
|
||||
if (type == IconType::Square)
|
||||
{
|
||||
if (filled)
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f;
|
||||
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
|
||||
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
|
||||
|
||||
drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
|
||||
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
|
||||
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
|
||||
|
||||
if (innerColor & 0xFF000000)
|
||||
drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments);
|
||||
|
||||
drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == IconType::Grid)
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f;
|
||||
const auto w = ceilf(r / 3.0f);
|
||||
|
||||
const auto baseTl = ImVec2(floorf(rect.center_x() - w * 2.5f), floorf(rect.center_y() - w * 2.5f));
|
||||
const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w));
|
||||
|
||||
auto tl = baseTl;
|
||||
auto br = baseBr;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
tl.x = baseTl.x;
|
||||
br.x = baseBr.x;
|
||||
drawList->AddRectFilled(tl, br, color);
|
||||
tl.x += w * 2;
|
||||
br.x += w * 2;
|
||||
if (i != 1 || filled)
|
||||
drawList->AddRectFilled(tl, br, color);
|
||||
tl.x += w * 2;
|
||||
br.x += w * 2;
|
||||
drawList->AddRectFilled(tl, br, color);
|
||||
|
||||
tl.y += w * 2;
|
||||
br.y += w * 2;
|
||||
}
|
||||
|
||||
triangleStart = br.x + w + 1.0f / 24.0f * rect.w;
|
||||
}
|
||||
|
||||
if (type == IconType::RoundSquare)
|
||||
{
|
||||
if (filled)
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f;
|
||||
const auto cr = r * 0.5f;
|
||||
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
|
||||
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
|
||||
|
||||
drawList->AddRectFilled(p0, p1, color, cr, 15);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
|
||||
const auto cr = r * 0.5f;
|
||||
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
|
||||
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
|
||||
|
||||
if (innerColor & 0xFF000000)
|
||||
drawList->AddRectFilled(p0, p1, innerColor, cr, 15);
|
||||
|
||||
drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale);
|
||||
}
|
||||
}
|
||||
else if (type == IconType::Diamond)
|
||||
{
|
||||
if (filled)
|
||||
{
|
||||
const auto r = 0.607f * rect.w / 2.0f;
|
||||
const auto c = rect.center();
|
||||
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0));
|
||||
drawList->PathFillConvex(color);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto r = 0.607f * rect.w / 2.0f - 0.5f;
|
||||
const auto c = rect.center();
|
||||
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r));
|
||||
drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0));
|
||||
|
||||
if (innerColor & 0xFF000000)
|
||||
drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
|
||||
|
||||
drawList->PathStroke(color, true, 2.0f * outline_scale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto triangleTip = triangleStart + rect.w * (0.45f - 0.32f);
|
||||
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(ceilf(triangleTip), rect.top() + rect.h * 0.5f),
|
||||
ImVec2(triangleStart, rect.center_y() + 0.15f * rect.h),
|
||||
ImVec2(triangleStart, rect.center_y() - 0.15f * rect.h),
|
||||
color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "ax/Widgets.h"
|
||||
#include "Interop.h"
|
||||
|
||||
void ax::Widgets::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, const ImVec4& innerColor/* = ImVec4(0, 0, 0, 0)*/)
|
||||
{
|
||||
if (ImGui::IsRectVisible(size))
|
||||
{
|
||||
auto cursorPos = ImGui::GetCursorScreenPos();
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
ax::Drawing::DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor));
|
||||
}
|
||||
|
||||
ImGui::Dummy(size);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
//------------------------------------------------------------------------------
|
||||
# ifndef __IMGUI_NODE_EDITOR_H__
|
||||
# define __IMGUI_NODE_EDITOR_H__
|
||||
# pragma once
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include <imgui.h>
|
||||
# include <cstdint> // std::uintXX_t
|
||||
# include <utility> // std::move
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
namespace ax {
|
||||
namespace NodeEditor {
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
struct NodeId;
|
||||
struct LinkId;
|
||||
struct PinId;
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
enum class SaveReasonFlags: uint32_t
|
||||
{
|
||||
None = 0x00000000,
|
||||
Navigation = 0x00000001,
|
||||
Position = 0x00000002,
|
||||
Size = 0x00000004,
|
||||
Selection = 0x00000008,
|
||||
User = 0x00000010
|
||||
};
|
||||
|
||||
inline SaveReasonFlags operator |(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)); }
|
||||
inline SaveReasonFlags operator &(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)); }
|
||||
|
||||
using ConfigSaveSettings = bool (*)(const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
|
||||
using ConfigLoadSettings = size_t (*)(char* data, void* userPointer);
|
||||
|
||||
using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
|
||||
using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, void* userPointer);
|
||||
|
||||
using ConfigSession = void (*)(void* userPointer);
|
||||
|
||||
struct Config
|
||||
{
|
||||
const char* SettingsFile;
|
||||
ConfigSession BeginSaveSession;
|
||||
ConfigSession EndSaveSession;
|
||||
ConfigSaveSettings SaveSettings;
|
||||
ConfigLoadSettings LoadSettings;
|
||||
ConfigSaveNodeSettings SaveNodeSettings;
|
||||
ConfigLoadNodeSettings LoadNodeSettings;
|
||||
void* UserPointer;
|
||||
|
||||
Config()
|
||||
: SettingsFile("NodeEditor.json")
|
||||
, BeginSaveSession(nullptr)
|
||||
, EndSaveSession(nullptr)
|
||||
, SaveSettings(nullptr)
|
||||
, LoadSettings(nullptr)
|
||||
, SaveNodeSettings(nullptr)
|
||||
, LoadNodeSettings(nullptr)
|
||||
, UserPointer(nullptr)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
enum class PinKind
|
||||
{
|
||||
Input,
|
||||
Output
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
enum StyleColor
|
||||
{
|
||||
StyleColor_Bg,
|
||||
StyleColor_Grid,
|
||||
StyleColor_NodeBg,
|
||||
StyleColor_NodeBorder,
|
||||
StyleColor_HovNodeBorder,
|
||||
StyleColor_SelNodeBorder,
|
||||
StyleColor_NodeSelRect,
|
||||
StyleColor_NodeSelRectBorder,
|
||||
StyleColor_HovLinkBorder,
|
||||
StyleColor_SelLinkBorder,
|
||||
StyleColor_LinkSelRect,
|
||||
StyleColor_LinkSelRectBorder,
|
||||
StyleColor_PinRect,
|
||||
StyleColor_PinRectBorder,
|
||||
StyleColor_Flow,
|
||||
StyleColor_FlowMarker,
|
||||
StyleColor_GroupBg,
|
||||
StyleColor_GroupBorder,
|
||||
|
||||
StyleColor_Count
|
||||
};
|
||||
|
||||
enum StyleVar
|
||||
{
|
||||
StyleVar_NodePadding,
|
||||
StyleVar_NodeRounding,
|
||||
StyleVar_NodeBorderWidth,
|
||||
StyleVar_HoveredNodeBorderWidth,
|
||||
StyleVar_SelectedNodeBorderWidth,
|
||||
StyleVar_PinRounding,
|
||||
StyleVar_PinBorderWidth,
|
||||
StyleVar_LinkStrength,
|
||||
StyleVar_SourceDirection,
|
||||
StyleVar_TargetDirection,
|
||||
StyleVar_ScrollDuration,
|
||||
StyleVar_FlowMarkerDistance,
|
||||
StyleVar_FlowSpeed,
|
||||
StyleVar_FlowDuration,
|
||||
StyleVar_PivotAlignment,
|
||||
StyleVar_PivotSize,
|
||||
StyleVar_PivotScale,
|
||||
StyleVar_PinCorners,
|
||||
StyleVar_PinRadius,
|
||||
StyleVar_PinArrowSize,
|
||||
StyleVar_PinArrowWidth,
|
||||
StyleVar_GroupRounding,
|
||||
StyleVar_GroupBorderWidth,
|
||||
};
|
||||
|
||||
struct Style
|
||||
{
|
||||
ImVec4 NodePadding;
|
||||
float NodeRounding;
|
||||
float NodeBorderWidth;
|
||||
float HoveredNodeBorderWidth;
|
||||
float SelectedNodeBorderWidth;
|
||||
float PinRounding;
|
||||
float PinBorderWidth;
|
||||
float LinkStrength;
|
||||
ImVec2 SourceDirection;
|
||||
ImVec2 TargetDirection;
|
||||
float ScrollDuration;
|
||||
float FlowMarkerDistance;
|
||||
float FlowSpeed;
|
||||
float FlowDuration;
|
||||
ImVec2 PivotAlignment;
|
||||
ImVec2 PivotSize;
|
||||
ImVec2 PivotScale;
|
||||
float PinCorners;
|
||||
float PinRadius;
|
||||
float PinArrowSize;
|
||||
float PinArrowWidth;
|
||||
float GroupRounding;
|
||||
float GroupBorderWidth;
|
||||
ImVec4 Colors[StyleColor_Count];
|
||||
|
||||
Style()
|
||||
{
|
||||
NodePadding = ImVec4(8, 8, 8, 8);
|
||||
NodeRounding = 12.0f;
|
||||
NodeBorderWidth = 1.5f;
|
||||
HoveredNodeBorderWidth = 3.5f;
|
||||
SelectedNodeBorderWidth = 3.5f;
|
||||
PinRounding = 4.0f;
|
||||
PinBorderWidth = 0.0f;
|
||||
LinkStrength = 100.0f;
|
||||
SourceDirection = ImVec2(1.0f, 0.0f);
|
||||
TargetDirection = ImVec2(-1.0f, 0.0f);
|
||||
ScrollDuration = 0.35f;
|
||||
FlowMarkerDistance = 30.0f;
|
||||
FlowSpeed = 150.0f;
|
||||
FlowDuration = 2.0f;
|
||||
PivotAlignment = ImVec2(0.5f, 0.5f);
|
||||
PivotSize = ImVec2(-1, -1);
|
||||
PivotScale = ImVec2(1, 1);
|
||||
PinCorners = ImDrawFlags_RoundCornersAll; // ImDrawCornerFlags_All renamed upstream in ImGui
|
||||
PinRadius = 0.0f;
|
||||
PinArrowSize = 0.0f;
|
||||
PinArrowWidth = 0.0f;
|
||||
GroupRounding = 6.0f;
|
||||
GroupBorderWidth = 1.0f;
|
||||
|
||||
Colors[StyleColor_Bg] = ImColor( 60, 60, 70, 200);
|
||||
Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40);
|
||||
Colors[StyleColor_NodeBg] = ImColor( 32, 32, 32, 200);
|
||||
Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96);
|
||||
Colors[StyleColor_HovNodeBorder] = ImColor( 50, 176, 255, 255);
|
||||
Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255);
|
||||
Colors[StyleColor_NodeSelRect] = ImColor( 5, 130, 255, 64);
|
||||
Colors[StyleColor_NodeSelRectBorder] = ImColor( 5, 130, 255, 128);
|
||||
Colors[StyleColor_HovLinkBorder] = ImColor( 50, 176, 255, 255);
|
||||
Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255);
|
||||
Colors[StyleColor_LinkSelRect] = ImColor( 5, 130, 255, 64);
|
||||
Colors[StyleColor_LinkSelRectBorder] = ImColor( 5, 130, 255, 128);
|
||||
Colors[StyleColor_PinRect] = ImColor( 60, 180, 255, 100);
|
||||
Colors[StyleColor_PinRectBorder] = ImColor( 60, 180, 255, 128);
|
||||
Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255);
|
||||
Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255);
|
||||
Colors[StyleColor_GroupBg] = ImColor( 0, 0, 0, 160);
|
||||
Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
struct EditorContext;
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
void SetCurrentEditor(EditorContext* ctx);
|
||||
EditorContext* GetCurrentEditor();
|
||||
EditorContext* CreateEditor(const Config* config = nullptr);
|
||||
void DestroyEditor(EditorContext* ctx);
|
||||
|
||||
Style& GetStyle();
|
||||
const char* GetStyleColorName(StyleColor colorIndex);
|
||||
|
||||
void PushStyleColor(StyleColor colorIndex, const ImVec4& color);
|
||||
void PopStyleColor(int count = 1);
|
||||
|
||||
void PushStyleVar(StyleVar varIndex, float value);
|
||||
void PushStyleVar(StyleVar varIndex, const ImVec2& value);
|
||||
void PushStyleVar(StyleVar varIndex, const ImVec4& value);
|
||||
void PopStyleVar(int count = 1);
|
||||
|
||||
void Begin(const char* id, const ImVec2& size = ImVec2(0, 0));
|
||||
void End();
|
||||
|
||||
void BeginNode(NodeId id);
|
||||
void BeginPin(PinId id, PinKind kind);
|
||||
void PinRect(const ImVec2& a, const ImVec2& b);
|
||||
void PinPivotRect(const ImVec2& a, const ImVec2& b);
|
||||
void PinPivotSize(const ImVec2& size);
|
||||
void PinPivotScale(const ImVec2& scale);
|
||||
void PinPivotAlignment(const ImVec2& alignment);
|
||||
void EndPin();
|
||||
void Group(const ImVec2& size);
|
||||
void EndNode();
|
||||
|
||||
bool BeginGroupHint(NodeId nodeId);
|
||||
ImVec2 GetGroupMin();
|
||||
ImVec2 GetGroupMax();
|
||||
ImDrawList* GetHintForegroundDrawList();
|
||||
ImDrawList* GetHintBackgroundDrawList();
|
||||
void EndGroupHint();
|
||||
|
||||
// TODO: Add a way to manage node background channels
|
||||
ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId);
|
||||
|
||||
bool Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
|
||||
|
||||
void Flow(LinkId linkId);
|
||||
|
||||
bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
|
||||
bool QueryNewLink(PinId* startId, PinId* endId);
|
||||
bool QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness = 1.0f);
|
||||
bool QueryNewNode(PinId* pinId);
|
||||
bool QueryNewNode(PinId* pinId, const ImVec4& color, float thickness = 1.0f);
|
||||
bool AcceptNewItem();
|
||||
bool AcceptNewItem(const ImVec4& color, float thickness = 1.0f);
|
||||
void RejectNewItem();
|
||||
void RejectNewItem(const ImVec4& color, float thickness = 1.0f);
|
||||
void EndCreate();
|
||||
|
||||
bool BeginDelete();
|
||||
bool QueryDeletedLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr);
|
||||
bool QueryDeletedNode(NodeId* nodeId);
|
||||
bool AcceptDeletedItem();
|
||||
void RejectDeletedItem();
|
||||
void EndDelete();
|
||||
|
||||
void SetNodePosition(NodeId nodeId, const ImVec2& editorPosition);
|
||||
ImVec2 GetNodePosition(NodeId nodeId);
|
||||
ImVec2 GetNodeSize(NodeId nodeId);
|
||||
void CenterNodeOnScreen(NodeId nodeId);
|
||||
|
||||
void RestoreNodeState(NodeId nodeId);
|
||||
|
||||
void Suspend();
|
||||
void Resume();
|
||||
bool IsSuspended();
|
||||
|
||||
bool IsActive();
|
||||
|
||||
bool HasSelectionChanged();
|
||||
int GetSelectedObjectCount();
|
||||
int GetSelectedNodes(NodeId* nodes, int size);
|
||||
int GetSelectedLinks(LinkId* links, int size);
|
||||
void ClearSelection();
|
||||
void SelectNode(NodeId nodeId, bool append = false);
|
||||
void SelectLink(LinkId linkId, bool append = false);
|
||||
void DeselectNode(NodeId nodeId);
|
||||
void DeselectLink(LinkId linkId);
|
||||
|
||||
bool DeleteNode(NodeId nodeId);
|
||||
bool DeleteLink(LinkId linkId);
|
||||
|
||||
void NavigateToContent(float duration = -1);
|
||||
void NavigateToSelection(bool zoomIn = false, float duration = -1);
|
||||
|
||||
bool ShowNodeContextMenu(NodeId* nodeId);
|
||||
bool ShowPinContextMenu(PinId* pinId);
|
||||
bool ShowLinkContextMenu(LinkId* linkId);
|
||||
bool ShowBackgroundContextMenu();
|
||||
|
||||
void EnableShortcuts(bool enable);
|
||||
bool AreShortcutsEnabled();
|
||||
|
||||
bool BeginShortcut();
|
||||
bool AcceptCut();
|
||||
bool AcceptCopy();
|
||||
bool AcceptPaste();
|
||||
bool AcceptDuplicate();
|
||||
bool AcceptCreateNode();
|
||||
int GetActionContextSize();
|
||||
int GetActionContextNodes(NodeId* nodes, int size);
|
||||
int GetActionContextLinks(LinkId* links, int size);
|
||||
void EndShortcut();
|
||||
|
||||
float GetCurrentZoom();
|
||||
|
||||
NodeId GetDoubleClickedNode();
|
||||
PinId GetDoubleClickedPin();
|
||||
LinkId GetDoubleClickedLink();
|
||||
bool IsBackgroundClicked();
|
||||
bool IsBackgroundDoubleClicked();
|
||||
|
||||
bool PinHadAnyLinks(PinId pinId);
|
||||
|
||||
ImVec2 GetScreenSize();
|
||||
ImVec2 ScreenToCanvas(const ImVec2& pos);
|
||||
ImVec2 CanvasToScreen(const ImVec2& pos);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
namespace Details {
|
||||
|
||||
template <typename T, typename Tag>
|
||||
struct SafeType
|
||||
{
|
||||
SafeType(T t)
|
||||
: m_Value(std::move(t))
|
||||
{
|
||||
}
|
||||
|
||||
SafeType(const SafeType&) = default;
|
||||
|
||||
template <typename T2, typename Tag2>
|
||||
SafeType(
|
||||
const SafeType
|
||||
<
|
||||
typename std::enable_if<!std::is_same<T, T2>::value, T2>::type,
|
||||
typename std::enable_if<!std::is_same<Tag, Tag2>::value, Tag2>::type
|
||||
>&) = delete;
|
||||
|
||||
SafeType& operator=(const SafeType&) = default;
|
||||
|
||||
explicit operator T() const { return Get(); }
|
||||
|
||||
T Get() const { return m_Value; }
|
||||
|
||||
private:
|
||||
T m_Value;
|
||||
};
|
||||
|
||||
|
||||
template <typename Tag>
|
||||
struct SafePointerType
|
||||
: SafeType<uintptr_t, Tag>
|
||||
{
|
||||
static const Tag Invalid;
|
||||
|
||||
using SafeType<uintptr_t, Tag>::SafeType;
|
||||
|
||||
SafePointerType()
|
||||
: SafePointerType(Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T = void> explicit SafePointerType(T* ptr): SafePointerType(reinterpret_cast<uintptr_t>(ptr)) {}
|
||||
template <typename T = void> T* AsPointer() const { return reinterpret_cast<T*>(this->Get()); }
|
||||
|
||||
explicit operator bool() const { return *this != Invalid; }
|
||||
};
|
||||
|
||||
template <typename Tag>
|
||||
const Tag SafePointerType<Tag>::Invalid = { 0 };
|
||||
|
||||
template <typename Tag>
|
||||
inline bool operator==(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
|
||||
{
|
||||
return lhs.Get() == rhs.Get();
|
||||
}
|
||||
|
||||
template <typename Tag>
|
||||
inline bool operator!=(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
|
||||
{
|
||||
return lhs.Get() != rhs.Get();
|
||||
}
|
||||
|
||||
} // namespace Details
|
||||
|
||||
struct NodeId final: Details::SafePointerType<NodeId>
|
||||
{
|
||||
using SafePointerType::SafePointerType;
|
||||
};
|
||||
|
||||
struct LinkId final: Details::SafePointerType<LinkId>
|
||||
{
|
||||
using SafePointerType::SafePointerType;
|
||||
};
|
||||
|
||||
struct PinId final: Details::SafePointerType<PinId>
|
||||
{
|
||||
using SafePointerType::SafePointerType;
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
} // namespace Editor
|
||||
} // namespace ax
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // __IMGUI_NODE_EDITOR_H__
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Michał Cichoń
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,814 @@
|
||||
// Crude implementation of JSON value object and parser.
|
||||
//
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
# include "crude_json.h"
|
||||
# include <iomanip>
|
||||
# include <limits>
|
||||
# include <cstdlib>
|
||||
# include <clocale>
|
||||
# include <cmath>
|
||||
# include <cstring>
|
||||
|
||||
|
||||
namespace crude_json {
|
||||
|
||||
value::value(value&& other)
|
||||
: m_Type(other.m_Type)
|
||||
{
|
||||
switch (m_Type)
|
||||
{
|
||||
case type_t::object: construct(m_Storage, std::move( *object_ptr(other.m_Storage))); break;
|
||||
case type_t::array: construct(m_Storage, std::move( *array_ptr(other.m_Storage))); break;
|
||||
case type_t::string: construct(m_Storage, std::move( *string_ptr(other.m_Storage))); break;
|
||||
case type_t::boolean: construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); break;
|
||||
case type_t::number: construct(m_Storage, std::move( *number_ptr(other.m_Storage))); break;
|
||||
default: break;
|
||||
}
|
||||
destruct(other.m_Storage, other.m_Type);
|
||||
other.m_Type = type_t::null;
|
||||
}
|
||||
|
||||
value::value(const value& other)
|
||||
: m_Type(other.m_Type)
|
||||
{
|
||||
switch (m_Type)
|
||||
{
|
||||
case type_t::object: construct(m_Storage, *object_ptr(other.m_Storage)); break;
|
||||
case type_t::array: construct(m_Storage, *array_ptr(other.m_Storage)); break;
|
||||
case type_t::string: construct(m_Storage, *string_ptr(other.m_Storage)); break;
|
||||
case type_t::boolean: construct(m_Storage, *boolean_ptr(other.m_Storage)); break;
|
||||
case type_t::number: construct(m_Storage, *number_ptr(other.m_Storage)); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
value& value::operator[](size_t index)
|
||||
{
|
||||
if (is_null())
|
||||
m_Type = construct(m_Storage, type_t::array);
|
||||
|
||||
if (is_array())
|
||||
{
|
||||
auto& v = *array_ptr(m_Storage);
|
||||
if (index >= v.size())
|
||||
v.insert(v.end(), index - v.size() + 1, value());
|
||||
|
||||
return v[index];
|
||||
}
|
||||
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
const value& value::operator[](size_t index) const
|
||||
{
|
||||
if (is_array())
|
||||
return (*array_ptr(m_Storage))[index];
|
||||
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
value& value::operator[](const string& key)
|
||||
{
|
||||
if (is_null())
|
||||
m_Type = construct(m_Storage, type_t::object);
|
||||
|
||||
if (is_object())
|
||||
return (*object_ptr(m_Storage))[key];
|
||||
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
const value& value::operator[](const string& key) const
|
||||
{
|
||||
if (is_object())
|
||||
{
|
||||
auto& o = *object_ptr(m_Storage);
|
||||
auto it = o.find(key);
|
||||
CRUDE_ASSERT(it != o.end());
|
||||
return it->second;
|
||||
}
|
||||
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
bool value::contains(const string& key) const
|
||||
{
|
||||
if (is_object())
|
||||
{
|
||||
auto& o = *object_ptr(m_Storage);
|
||||
auto it = o.find(key);
|
||||
return it != o.end();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void value::push_back(const value& value)
|
||||
{
|
||||
if (is_null())
|
||||
m_Type = construct(m_Storage, type_t::array);
|
||||
|
||||
if (is_array())
|
||||
{
|
||||
auto& v = *array_ptr(m_Storage);
|
||||
v.push_back(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
}
|
||||
|
||||
void value::push_back(value&& value)
|
||||
{
|
||||
if (is_null())
|
||||
m_Type = construct(m_Storage, type_t::array);
|
||||
|
||||
if (is_array())
|
||||
{
|
||||
auto& v = *array_ptr(m_Storage);
|
||||
v.push_back(std::move(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
CRUDE_ASSERT(false && "operator[] on unsupported type");
|
||||
std::terminate();
|
||||
}
|
||||
}
|
||||
|
||||
void value::swap(value& other)
|
||||
{
|
||||
using std::swap;
|
||||
|
||||
if (m_Type == other.m_Type)
|
||||
{
|
||||
switch (m_Type)
|
||||
{
|
||||
case type_t::object: swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); break;
|
||||
case type_t::array: swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); break;
|
||||
case type_t::string: swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); break;
|
||||
case type_t::boolean: swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); break;
|
||||
case type_t::number: swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value tmp(std::move(other));
|
||||
other.~value();
|
||||
new (&other) value(std::move(*this));
|
||||
this->~value();
|
||||
new (this) value(std::move(tmp));
|
||||
}
|
||||
}
|
||||
|
||||
string value::dump(const int indent, const char indent_char) const
|
||||
{
|
||||
dump_context_t context(indent, indent_char);
|
||||
|
||||
context.out.precision(std::numeric_limits<double>::max_digits10 + 1);
|
||||
context.out << std::defaultfloat;
|
||||
|
||||
dump(context, 0);
|
||||
return context.out.str();
|
||||
}
|
||||
|
||||
void value::dump_context_t::write_indent(int level)
|
||||
{
|
||||
if (indent <= 0 || level == 0)
|
||||
return;
|
||||
|
||||
out.fill(indent_char);
|
||||
out.width(indent * level);
|
||||
out << indent_char;
|
||||
out.width(0);
|
||||
}
|
||||
|
||||
void value::dump_context_t::write_separator()
|
||||
{
|
||||
if (indent < 0)
|
||||
return;
|
||||
|
||||
out.put(' ');
|
||||
}
|
||||
|
||||
void value::dump_context_t::write_newline()
|
||||
{
|
||||
if (indent < 0)
|
||||
return;
|
||||
|
||||
out.put('\n');
|
||||
}
|
||||
|
||||
void value::dump(dump_context_t& context, int level) const
|
||||
{
|
||||
context.write_indent(level);
|
||||
|
||||
switch (m_Type)
|
||||
{
|
||||
case type_t::null:
|
||||
context.out << "null";
|
||||
break;
|
||||
|
||||
case type_t::object:
|
||||
context.out << '{';
|
||||
{
|
||||
context.write_newline();
|
||||
bool first = true;
|
||||
for (auto& entry : *object_ptr(m_Storage))
|
||||
{
|
||||
if (!first) { context.out << ','; context.write_newline(); } else first = false;
|
||||
context.write_indent(level + 1);
|
||||
context.out << '\"' << entry.first << "\":";
|
||||
if (!entry.second.is_structured())
|
||||
{
|
||||
context.write_separator();
|
||||
entry.second.dump(context, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.write_newline();
|
||||
entry.second.dump(context, level + 1);
|
||||
}
|
||||
}
|
||||
if (!first)
|
||||
context.write_newline();
|
||||
}
|
||||
context.write_indent(level);
|
||||
context.out << '}';
|
||||
break;
|
||||
|
||||
case type_t::array:
|
||||
context.out << '[';
|
||||
{
|
||||
context.write_newline();
|
||||
bool first = true;
|
||||
for (auto& entry : *array_ptr(m_Storage))
|
||||
{
|
||||
if (!first) { context.out << ','; context.write_newline(); } else first = false;
|
||||
if (!entry.is_structured())
|
||||
{
|
||||
context.write_indent(level + 1);
|
||||
entry.dump(context, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.dump(context, level + 1);
|
||||
}
|
||||
}
|
||||
if (!first)
|
||||
context.write_newline();
|
||||
}
|
||||
context.write_indent(level);
|
||||
context.out << ']';
|
||||
break;
|
||||
|
||||
case type_t::string:
|
||||
context.out << '\"';
|
||||
|
||||
if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != string::npos || string_ptr(m_Storage)->find('\0') != string::npos)
|
||||
{
|
||||
for (auto c : *string_ptr(m_Storage))
|
||||
{
|
||||
if (c == '\"') context.out << "\\\"";
|
||||
else if (c == '\\') context.out << "\\\\";
|
||||
else if (c == '/') context.out << "\\/";
|
||||
else if (c == '\b') context.out << "\\b";
|
||||
else if (c == '\f') context.out << "\\f";
|
||||
else if (c == '\n') context.out << "\\n";
|
||||
else if (c == '\r') context.out << "\\r";
|
||||
else if (c == '\t') context.out << "\\t";
|
||||
else if (c == 0) context.out << "\\u0000";
|
||||
else context.out << c;
|
||||
}
|
||||
}
|
||||
else
|
||||
context.out << *string_ptr(m_Storage);
|
||||
context.out << '\"';
|
||||
break;
|
||||
|
||||
|
||||
case type_t::boolean:
|
||||
if (*boolean_ptr(m_Storage))
|
||||
context.out << "true";
|
||||
else
|
||||
context.out << "false";
|
||||
break;
|
||||
|
||||
case type_t::number:
|
||||
context.out << *number_ptr(m_Storage);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct value::parser
|
||||
{
|
||||
parser(const char* begin, const char* end)
|
||||
: m_Cursor(begin)
|
||||
, m_End(end)
|
||||
{
|
||||
}
|
||||
|
||||
value parse()
|
||||
{
|
||||
value v;
|
||||
|
||||
// Switch to C locale to make strtod and strtol work as expected
|
||||
auto previous_locale = std::setlocale(LC_NUMERIC, "C");
|
||||
|
||||
// Accept single value only when end of the stream is reached.
|
||||
if (!accept_element(v) || !eof())
|
||||
v = value(type_t::discarded);
|
||||
|
||||
if (previous_locale && strcmp(previous_locale, "C") != 0)
|
||||
std::setlocale(LC_NUMERIC, previous_locale);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private:
|
||||
struct cursor_state
|
||||
{
|
||||
cursor_state(parser* p)
|
||||
: m_Owner(p)
|
||||
, m_LastCursor(p->m_Cursor)
|
||||
{
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
m_Owner->m_Cursor = m_LastCursor;
|
||||
}
|
||||
|
||||
bool operator()(bool accept)
|
||||
{
|
||||
if (!accept)
|
||||
reset();
|
||||
else
|
||||
m_LastCursor = m_Owner->m_Cursor;
|
||||
return accept;
|
||||
}
|
||||
|
||||
private:
|
||||
parser* m_Owner;
|
||||
const char* m_LastCursor;
|
||||
};
|
||||
|
||||
cursor_state state()
|
||||
{
|
||||
return cursor_state(this);
|
||||
}
|
||||
|
||||
bool accept_value(value& result)
|
||||
{
|
||||
return accept_object(result)
|
||||
|| accept_array(result)
|
||||
|| accept_string(result)
|
||||
|| accept_number(result)
|
||||
|| accept_boolean(result)
|
||||
|| accept_null(result);
|
||||
}
|
||||
|
||||
bool accept_object(value& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
object o;
|
||||
if (s(accept('{') && accept_ws() && accept('}')))
|
||||
{
|
||||
result = o;
|
||||
return true;
|
||||
}
|
||||
else if (s(accept('{') && accept_members(o) && accept('}')))
|
||||
{
|
||||
result = std::move(o);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_members(object& o)
|
||||
{
|
||||
if (!accept_member(o))
|
||||
return false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
auto s = state();
|
||||
if (!s(accept(',') && accept_member(o)))
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_member(object& o)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
value key;
|
||||
value v;
|
||||
if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && accept_element(v)))
|
||||
{
|
||||
o.emplace(std::move(key.get<string>()), std::move(v));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_array(value& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
if (s(accept('[') && accept_ws() && accept(']')))
|
||||
{
|
||||
result = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
array a;
|
||||
if (s(accept('[') && accept_elements(a) && accept(']')))
|
||||
{
|
||||
result = std::move(a);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_elements(array& a)
|
||||
{
|
||||
value v;
|
||||
if (!accept_element(v))
|
||||
return false;
|
||||
|
||||
a.emplace_back(std::move(v));
|
||||
while (true)
|
||||
{
|
||||
auto s = state();
|
||||
v = nullptr;
|
||||
if (!s(accept(',') && accept_element(v)))
|
||||
break;
|
||||
a.emplace_back(std::move(v));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_element(value& result)
|
||||
{
|
||||
auto s = state();
|
||||
return s(accept_ws() && accept_value(result) && accept_ws());
|
||||
}
|
||||
|
||||
bool accept_string(value& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
string v;
|
||||
if (s(accept('\"') && accept_characters(v) && accept('\"')))
|
||||
{
|
||||
result = std::move(v);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_characters(string& result)
|
||||
{
|
||||
int c;
|
||||
while (accept_character(c))
|
||||
{
|
||||
CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8
|
||||
result.push_back(static_cast<char>(c));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_character(int& c)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
if (accept('\\'))
|
||||
{
|
||||
return accept_escape(c);
|
||||
}
|
||||
else if (expect('\"'))
|
||||
return false;
|
||||
|
||||
// #todo: Handle UTF-8 sequences.
|
||||
return s((c = peek()) >= 0) && advance();
|
||||
}
|
||||
|
||||
bool accept_escape(int& c)
|
||||
{
|
||||
if (accept('\"')) { c = '\"'; return true; }
|
||||
if (accept('\\')) { c = '\\'; return true; }
|
||||
if (accept('/')) { c = '/'; return true; }
|
||||
if (accept('b')) { c = '\b'; return true; }
|
||||
if (accept('f')) { c = '\f'; return true; }
|
||||
if (accept('n')) { c = '\n'; return true; }
|
||||
if (accept('r')) { c = '\r'; return true; }
|
||||
if (accept('t')) { c = '\t'; return true; }
|
||||
|
||||
auto s = state();
|
||||
|
||||
string hex;
|
||||
hex.reserve(4);
|
||||
if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && accept_hex(hex) && accept_hex(hex)))
|
||||
{
|
||||
char* end = nullptr;
|
||||
auto v = std::strtol(hex.c_str(), &end, 16);
|
||||
if (end != hex.c_str() + hex.size())
|
||||
return false;
|
||||
|
||||
c = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_hex(string& result)
|
||||
{
|
||||
if (accept_digit(result))
|
||||
return true;
|
||||
|
||||
auto c = peek();
|
||||
if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
|
||||
{
|
||||
advance();
|
||||
result.push_back(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_number(value& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
string n;
|
||||
if (s(accept_int(n) && accept_frac(n) && accept_exp(n)))
|
||||
{
|
||||
char* end = nullptr;
|
||||
auto v = std::strtod(n.c_str(), &end);
|
||||
if (end != n.c_str() + n.size())
|
||||
return false;
|
||||
|
||||
if (!std::isnormal(v))
|
||||
return false;
|
||||
|
||||
result = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_int(string& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
string part;
|
||||
if (s(accept_onenine(part) && accept_digits(part)))
|
||||
{
|
||||
result += std::move(part);
|
||||
return true;
|
||||
}
|
||||
|
||||
part.resize(0);
|
||||
if (accept_digit(part))
|
||||
{
|
||||
result += std::move(part);
|
||||
return true;
|
||||
}
|
||||
|
||||
part.resize(0);
|
||||
if (s(accept('-') && accept_onenine(part) && accept_digits(part)))
|
||||
{
|
||||
result += '-';
|
||||
result += std::move(part);
|
||||
return true;
|
||||
}
|
||||
|
||||
part.resize(0);
|
||||
if (s(accept('-') && accept_digit(part)))
|
||||
{
|
||||
result += '-';
|
||||
result += std::move(part);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_digits(string& result)
|
||||
{
|
||||
string part;
|
||||
if (!accept_digit(part))
|
||||
return false;
|
||||
|
||||
while (accept_digit(part))
|
||||
;
|
||||
|
||||
result += std::move(part);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_digit(string& result)
|
||||
{
|
||||
if (accept('0'))
|
||||
{
|
||||
result.push_back('0');
|
||||
return true;
|
||||
}
|
||||
else if (accept_onenine(result))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_onenine(string& result)
|
||||
{
|
||||
auto c = peek();
|
||||
if (c >= '1' && c <= '9')
|
||||
{
|
||||
result.push_back(static_cast<char>(c));
|
||||
return advance();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_frac(string& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
string part;
|
||||
if (s(accept('.') && accept_digits(part)))
|
||||
{
|
||||
result += '.';
|
||||
result += std::move(part);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_exp(string& result)
|
||||
{
|
||||
auto s = state();
|
||||
|
||||
string part;
|
||||
if (s(accept('e') && accept_sign(part) && accept_digits(part)))
|
||||
{
|
||||
result += 'e';
|
||||
result += std::move(part);
|
||||
return true;
|
||||
}
|
||||
part.resize(0);
|
||||
if (s(accept('E') && accept_sign(part) && accept_digits(part)))
|
||||
{
|
||||
result += 'E';
|
||||
result += std::move(part);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_sign(string& result)
|
||||
{
|
||||
if (accept('+'))
|
||||
result.push_back('+');
|
||||
else if (accept('-'))
|
||||
result.push_back('-');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_ws()
|
||||
{
|
||||
while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20'))
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool accept_boolean(value& result)
|
||||
{
|
||||
if (accept("true"))
|
||||
{
|
||||
result = true;
|
||||
return true;
|
||||
}
|
||||
else if (accept("false"))
|
||||
{
|
||||
result = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept_null(value& result)
|
||||
{
|
||||
if (accept("null"))
|
||||
{
|
||||
result = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept(char c)
|
||||
{
|
||||
if (expect(c))
|
||||
return advance();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accept(const char* str)
|
||||
{
|
||||
auto last = m_Cursor;
|
||||
|
||||
while (*str)
|
||||
{
|
||||
if (eof() || *str != *m_Cursor)
|
||||
{
|
||||
m_Cursor = last;
|
||||
return false;
|
||||
}
|
||||
|
||||
advance();
|
||||
++str;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int peek() const
|
||||
{
|
||||
if (!eof())
|
||||
return *m_Cursor;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool expect(char c)
|
||||
{
|
||||
return peek() == c;
|
||||
}
|
||||
|
||||
bool advance(int count = 1)
|
||||
{
|
||||
if (m_Cursor + count > m_End)
|
||||
{
|
||||
m_Cursor = m_End;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Cursor += count;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool eof() const
|
||||
{
|
||||
return m_Cursor == m_End;
|
||||
}
|
||||
|
||||
const char* m_Cursor;
|
||||
const char* m_End;
|
||||
};
|
||||
|
||||
value value::parse(const string& data)
|
||||
{
|
||||
auto p = parser(data.c_str(), data.c_str() + data.size());
|
||||
|
||||
auto v = p.parse();
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace crude_json
|
||||
@@ -0,0 +1,223 @@
|
||||
// Crude implementation of JSON value object and parser.
|
||||
//
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
# ifndef __CRUDE_JSON_H__
|
||||
# define __CRUDE_JSON_H__
|
||||
# pragma once
|
||||
|
||||
# include <type_traits>
|
||||
# include <string>
|
||||
# include <vector>
|
||||
# include <map>
|
||||
# include <cstddef>
|
||||
# include <algorithm>
|
||||
# include <sstream>
|
||||
|
||||
# ifndef CRUDE_ASSERT
|
||||
# include <cassert>
|
||||
# define CRUDE_ASSERT(expr) assert(expr)
|
||||
# endif
|
||||
|
||||
namespace crude_json {
|
||||
|
||||
struct value;
|
||||
|
||||
using string = std::string;
|
||||
using object = std::map<string, value>;
|
||||
using array = std::vector<value>;
|
||||
using number = double;
|
||||
using boolean = bool;
|
||||
using null = std::nullptr_t;
|
||||
|
||||
enum class type_t
|
||||
{
|
||||
null,
|
||||
object,
|
||||
array,
|
||||
string,
|
||||
boolean,
|
||||
number,
|
||||
discarded
|
||||
};
|
||||
|
||||
struct value
|
||||
{
|
||||
value(type_t type = type_t::null): m_Type(construct(m_Storage, type)) {}
|
||||
value(value&& other);
|
||||
value(const value& other);
|
||||
|
||||
value( null) : m_Type(construct(m_Storage, null())) {}
|
||||
value( object&& v): m_Type(construct(m_Storage, std::move(v))) {}
|
||||
value(const object& v): m_Type(construct(m_Storage, v)) {}
|
||||
value( array&& v): m_Type(construct(m_Storage, std::move(v))) {}
|
||||
value(const array& v): m_Type(construct(m_Storage, v)) {}
|
||||
value( string&& v): m_Type(construct(m_Storage, std::move(v))) {}
|
||||
value(const string& v): m_Type(construct(m_Storage, v)) {}
|
||||
value(const char* v): m_Type(construct(m_Storage, v)) {}
|
||||
value( boolean v): m_Type(construct(m_Storage, v)) {}
|
||||
value( number v): m_Type(construct(m_Storage, v)) {}
|
||||
~value() { destruct(m_Storage, m_Type); }
|
||||
|
||||
value& operator=(value&& other) { if (this != &other) { value(std::move(other)).swap(*this); } return *this; }
|
||||
value& operator=(const value& other) { if (this != &other) { value( other).swap(*this); } return *this; }
|
||||
|
||||
value& operator=( null) { auto other = value( ); swap(other); return *this; }
|
||||
value& operator=( object&& v) { auto other = value(std::move(v)); swap(other); return *this; }
|
||||
value& operator=(const object& v) { auto other = value( v); swap(other); return *this; }
|
||||
value& operator=( array&& v) { auto other = value(std::move(v)); swap(other); return *this; }
|
||||
value& operator=(const array& v) { auto other = value( v); swap(other); return *this; }
|
||||
value& operator=( string&& v) { auto other = value(std::move(v)); swap(other); return *this; }
|
||||
value& operator=(const string& v) { auto other = value( v); swap(other); return *this; }
|
||||
value& operator=(const char* v) { auto other = value( v); swap(other); return *this; }
|
||||
value& operator=( boolean v) { auto other = value( v); swap(other); return *this; }
|
||||
value& operator=( number v) { auto other = value( v); swap(other); return *this; }
|
||||
|
||||
type_t type() const { return m_Type; }
|
||||
|
||||
operator type_t() const { return m_Type; }
|
||||
|
||||
value& operator[](size_t index);
|
||||
const value& operator[](size_t index) const;
|
||||
value& operator[](const string& key);
|
||||
const value& operator[](const string& key) const;
|
||||
|
||||
bool contains(const string& key) const;
|
||||
|
||||
void push_back(const value& value);
|
||||
void push_back(value&& value);
|
||||
|
||||
bool is_primitive() const { return is_string() || is_number() || is_boolean() || is_null(); }
|
||||
bool is_structured() const { return is_object() || is_array(); }
|
||||
bool is_null() const { return m_Type == type_t::null; }
|
||||
bool is_object() const { return m_Type == type_t::object; }
|
||||
bool is_array() const { return m_Type == type_t::array; }
|
||||
bool is_string() const { return m_Type == type_t::string; }
|
||||
bool is_boolean() const { return m_Type == type_t::boolean; }
|
||||
bool is_number() const { return m_Type == type_t::number; }
|
||||
bool is_discarded() const { return m_Type == type_t::discarded; }
|
||||
|
||||
template <typename T> const T& get() const;
|
||||
template <typename T> T& get();
|
||||
|
||||
string dump(const int indent = -1, const char indent_char = ' ') const;
|
||||
|
||||
void swap(value& other);
|
||||
|
||||
inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); }
|
||||
|
||||
// Returns discarded value for invalid inputs.
|
||||
static value parse(const string& data);
|
||||
|
||||
private:
|
||||
struct parser;
|
||||
|
||||
// VS2015: std::max() is not constexpr yet.
|
||||
# define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a))
|
||||
# define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c)
|
||||
# define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d)
|
||||
# define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e)
|
||||
enum
|
||||
{
|
||||
max_size = CRUDE_MAX5( sizeof(string), sizeof(object), sizeof(array), sizeof(number), sizeof(boolean)),
|
||||
max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), alignof(number), alignof(boolean))
|
||||
};
|
||||
# undef CRUDE_MAX5
|
||||
# undef CRUDE_MAX4
|
||||
# undef CRUDE_MAX3
|
||||
# undef CRUDE_MAX2
|
||||
using storage_t = std::aligned_storage<max_size, max_align>::type;
|
||||
|
||||
static object* object_ptr( storage_t& storage) { return reinterpret_cast< object*>(&storage); }
|
||||
static const object* object_ptr(const storage_t& storage) { return reinterpret_cast<const object*>(&storage); }
|
||||
static array* array_ptr( storage_t& storage) { return reinterpret_cast< array*>(&storage); }
|
||||
static const array* array_ptr(const storage_t& storage) { return reinterpret_cast<const array*>(&storage); }
|
||||
static string* string_ptr( storage_t& storage) { return reinterpret_cast< string*>(&storage); }
|
||||
static const string* string_ptr(const storage_t& storage) { return reinterpret_cast<const string*>(&storage); }
|
||||
static boolean* boolean_ptr( storage_t& storage) { return reinterpret_cast< boolean*>(&storage); }
|
||||
static const boolean* boolean_ptr(const storage_t& storage) { return reinterpret_cast<const boolean*>(&storage); }
|
||||
static number* number_ptr( storage_t& storage) { return reinterpret_cast< number*>(&storage); }
|
||||
static const number* number_ptr(const storage_t& storage) { return reinterpret_cast<const number*>(&storage); }
|
||||
|
||||
static type_t construct(storage_t& storage, type_t type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case type_t::object: new (&storage) object(); break;
|
||||
case type_t::array: new (&storage) array(); break;
|
||||
case type_t::string: new (&storage) string(); break;
|
||||
case type_t::boolean: new (&storage) boolean(); break;
|
||||
case type_t::number: new (&storage) number(); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
static type_t construct(storage_t& storage, null) { return type_t::null; }
|
||||
static type_t construct(storage_t& storage, object&& value) { new (&storage) object(std::forward<object>(value)); return type_t::object; }
|
||||
static type_t construct(storage_t& storage, const object& value) { new (&storage) object(value); return type_t::object; }
|
||||
static type_t construct(storage_t& storage, array&& value) { new (&storage) array(std::forward<array>(value)); return type_t::array; }
|
||||
static type_t construct(storage_t& storage, const array& value) { new (&storage) array(value); return type_t::array; }
|
||||
static type_t construct(storage_t& storage, string&& value) { new (&storage) string(std::forward<string>(value)); return type_t::string; }
|
||||
static type_t construct(storage_t& storage, const string& value) { new (&storage) string(value); return type_t::string; }
|
||||
static type_t construct(storage_t& storage, const char* value) { new (&storage) string(value); return type_t::string; }
|
||||
static type_t construct(storage_t& storage, boolean value) { new (&storage) boolean(value); return type_t::boolean; }
|
||||
static type_t construct(storage_t& storage, number value) { new (&storage) number(value); return type_t::number; }
|
||||
|
||||
static void destruct(storage_t& storage, type_t type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case type_t::object: object_ptr(storage)->~object(); break;
|
||||
case type_t::array: array_ptr(storage)->~array(); break;
|
||||
case type_t::string: string_ptr(storage)->~string(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
struct dump_context_t
|
||||
{
|
||||
std::ostringstream out;
|
||||
const int indent = -1;
|
||||
const char indent_char = ' ';
|
||||
|
||||
// VS2015: Aggregate initialization isn't a thing yet.
|
||||
dump_context_t(const int indent, const char indent_char)
|
||||
: indent(indent)
|
||||
, indent_char(indent_char)
|
||||
{
|
||||
}
|
||||
|
||||
void write_indent(int level);
|
||||
void write_separator();
|
||||
void write_newline();
|
||||
};
|
||||
|
||||
void dump(dump_context_t& context, int level) const;
|
||||
|
||||
storage_t m_Storage;
|
||||
type_t m_Type;
|
||||
};
|
||||
|
||||
template <> inline const object& value::get<object>() const { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
|
||||
template <> inline const array& value::get<array>() const { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
|
||||
template <> inline const string& value::get<string>() const { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
|
||||
template <> inline const boolean& value::get<boolean>() const { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
|
||||
template <> inline const number& value::get<number>() const { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
|
||||
|
||||
template <> inline object& value::get<object>() { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
|
||||
template <> inline array& value::get<array>() { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
|
||||
template <> inline string& value::get<string>() { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
|
||||
template <> inline boolean& value::get<boolean>() { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
|
||||
template <> inline number& value::get<number>() { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
|
||||
|
||||
|
||||
} // namespace crude_json
|
||||
|
||||
# endif // __CRUDE_JSON_H__
|
||||
@@ -0,0 +1,142 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
//------------------------------------------------------------------------------
|
||||
# ifndef __IMGUI_BEZIER_MATH_H__
|
||||
# define __IMGUI_BEZIER_MATH_H__
|
||||
# pragma once
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include "imgui_extra_math.h"
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
template <typename T>
|
||||
struct ImCubicBezierPointsT
|
||||
{
|
||||
T P0;
|
||||
T P1;
|
||||
T P2;
|
||||
T P3;
|
||||
};
|
||||
using ImCubicBezierPoints = ImCubicBezierPointsT<ImVec2>;
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Low-level Bezier curve sampling.
|
||||
template <typename T> inline T ImLinearBezier(const T& p0, const T& p1, float t);
|
||||
template <typename T> inline T ImLinearBezierDt(const T& p0, const T& p1, float t);
|
||||
template <typename T> inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t);
|
||||
template <typename T> inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t);
|
||||
template <typename T> inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t);
|
||||
template <typename T> inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t);
|
||||
|
||||
|
||||
// High-level Bezier sampling, automatically collapse to lower level Bezier curves if control points overlap.
|
||||
template <typename T> inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t);
|
||||
template <typename T> inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t);
|
||||
template <typename T> inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t);
|
||||
template <typename T> inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t);
|
||||
|
||||
|
||||
// Calculate approximate length of Cubic Bezier curve.
|
||||
template <typename T> inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3);
|
||||
template <typename T> inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve);
|
||||
|
||||
|
||||
// Splits Cubic Bezier curve into two curves.
|
||||
template <typename T>
|
||||
struct ImCubicBezierSplitResultT
|
||||
{
|
||||
ImCubicBezierPointsT<T> Left;
|
||||
ImCubicBezierPointsT<T> Right;
|
||||
};
|
||||
using ImCubicBezierSplitResult = ImCubicBezierSplitResultT<ImVec2>;
|
||||
|
||||
template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t);
|
||||
template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t);
|
||||
|
||||
|
||||
// Returns bounding rectangle of Cubic Bezier curve.
|
||||
inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3);
|
||||
inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve);
|
||||
|
||||
|
||||
// Project point on Cubic Bezier curve.
|
||||
struct ImProjectResult
|
||||
{
|
||||
ImVec2 Point; // Point on curve
|
||||
float Time; // [0 - 1]
|
||||
float Distance; // Distance to curve
|
||||
};
|
||||
|
||||
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions = 100);
|
||||
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions = 100);
|
||||
|
||||
|
||||
// Calculate intersection between line and a Cubic Bezier curve.
|
||||
struct ImCubicBezierIntersectResult
|
||||
{
|
||||
int Count;
|
||||
ImVec2 Points[3];
|
||||
};
|
||||
|
||||
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1);
|
||||
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line);
|
||||
|
||||
|
||||
// Adaptive Cubic Bezier subdivision.
|
||||
enum ImCubicBezierSubdivideFlags
|
||||
{
|
||||
ImCubicBezierSubdivide_None = 0,
|
||||
ImCubicBezierSubdivide_SkipFirst = 1
|
||||
};
|
||||
|
||||
struct ImCubicBezierSubdivideSample
|
||||
{
|
||||
ImVec2 Point;
|
||||
ImVec2 Tangent;
|
||||
};
|
||||
|
||||
using ImCubicBezierSubdivideCallback = void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer);
|
||||
|
||||
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
|
||||
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
|
||||
|
||||
|
||||
// F has signature void(const ImCubicBezierSubdivideSample& p)
|
||||
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
|
||||
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
|
||||
|
||||
// Fixed step Cubic Bezier subdivision.
|
||||
struct ImCubicBezierFixedStepSample
|
||||
{
|
||||
float T;
|
||||
float Length;
|
||||
ImVec2 Point;
|
||||
bool BreakSearch;
|
||||
};
|
||||
|
||||
using ImCubicBezierFixedStepCallback = void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer);
|
||||
|
||||
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
|
||||
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
|
||||
|
||||
|
||||
// F has signature void(const ImCubicBezierFixedStepSample& p)
|
||||
template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
|
||||
template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include "imgui_bezier_math.inl"
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // __IMGUI_BEZIER_MATH_H__
|
||||
@@ -0,0 +1,668 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
//------------------------------------------------------------------------------
|
||||
# ifndef __IMGUI_BEZIER_MATH_INL__
|
||||
# define __IMGUI_BEZIER_MATH_INL__
|
||||
# pragma once
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include "imgui_bezier_math.h"
|
||||
# include <map> // used in ImCubicBezierFixedStep
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
template <typename T>
|
||||
inline T ImLinearBezier(const T& p0, const T& p1, float t)
|
||||
{
|
||||
return p0 + t * (p1 - p0);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImLinearBezierDt(const T& p0, const T& p1, float t)
|
||||
{
|
||||
return p1 - p0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t)
|
||||
{
|
||||
const auto a = 1 - t;
|
||||
|
||||
return a * a * p0 + 2 * t * a * p1 + t * t * p2;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t)
|
||||
{
|
||||
return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t)
|
||||
{
|
||||
const auto a = 1 - t;
|
||||
const auto b = a * a * a;
|
||||
const auto c = t * t * t;
|
||||
|
||||
return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t)
|
||||
{
|
||||
const auto a = 1 - t;
|
||||
const auto b = a * a;
|
||||
const auto c = t * t;
|
||||
const auto d = 2 * t * a;
|
||||
|
||||
return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t)
|
||||
{
|
||||
const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
|
||||
const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
|
||||
|
||||
if (cp0_zero && cp1_zero)
|
||||
return ImLinearBezier(p0, p3, t);
|
||||
else if (cp0_zero)
|
||||
return ImQuadraticBezier(p0, p2, p3, t);
|
||||
else if (cp1_zero)
|
||||
return ImQuadraticBezier(p0, p1, p3, t);
|
||||
else
|
||||
return ImCubicBezier(p0, p1, p2, p3, t);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t)
|
||||
{
|
||||
return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t)
|
||||
{
|
||||
const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
|
||||
const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
|
||||
|
||||
if (cp0_zero && cp1_zero)
|
||||
return ImLinearBezierDt(p0, p3, t);
|
||||
else if (cp0_zero)
|
||||
return ImQuadraticBezierDt(p0, p2, p3, t);
|
||||
else if (cp1_zero)
|
||||
return ImQuadraticBezierDt(p0, p1, p3, t);
|
||||
else
|
||||
return ImCubicBezierDt(p0, p1, p2, p3, t);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t)
|
||||
{
|
||||
return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3)
|
||||
{
|
||||
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
|
||||
static const float t_values[] =
|
||||
{
|
||||
-0.0640568928626056260850430826247450385909f,
|
||||
0.0640568928626056260850430826247450385909f,
|
||||
-0.1911188674736163091586398207570696318404f,
|
||||
0.1911188674736163091586398207570696318404f,
|
||||
-0.3150426796961633743867932913198102407864f,
|
||||
0.3150426796961633743867932913198102407864f,
|
||||
-0.4337935076260451384870842319133497124524f,
|
||||
0.4337935076260451384870842319133497124524f,
|
||||
-0.5454214713888395356583756172183723700107f,
|
||||
0.5454214713888395356583756172183723700107f,
|
||||
-0.6480936519369755692524957869107476266696f,
|
||||
0.6480936519369755692524957869107476266696f,
|
||||
-0.7401241915785543642438281030999784255232f,
|
||||
0.7401241915785543642438281030999784255232f,
|
||||
-0.8200019859739029219539498726697452080761f,
|
||||
0.8200019859739029219539498726697452080761f,
|
||||
-0.8864155270044010342131543419821967550873f,
|
||||
0.8864155270044010342131543419821967550873f,
|
||||
-0.9382745520027327585236490017087214496548f,
|
||||
0.9382745520027327585236490017087214496548f,
|
||||
-0.9747285559713094981983919930081690617411f,
|
||||
0.9747285559713094981983919930081690617411f,
|
||||
-0.9951872199970213601799974097007368118745f,
|
||||
0.9951872199970213601799974097007368118745f
|
||||
};
|
||||
|
||||
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
|
||||
static const float c_values[] =
|
||||
{
|
||||
0.1279381953467521569740561652246953718517f,
|
||||
0.1279381953467521569740561652246953718517f,
|
||||
0.1258374563468282961213753825111836887264f,
|
||||
0.1258374563468282961213753825111836887264f,
|
||||
0.1216704729278033912044631534762624256070f,
|
||||
0.1216704729278033912044631534762624256070f,
|
||||
0.1155056680537256013533444839067835598622f,
|
||||
0.1155056680537256013533444839067835598622f,
|
||||
0.1074442701159656347825773424466062227946f,
|
||||
0.1074442701159656347825773424466062227946f,
|
||||
0.0976186521041138882698806644642471544279f,
|
||||
0.0976186521041138882698806644642471544279f,
|
||||
0.0861901615319532759171852029837426671850f,
|
||||
0.0861901615319532759171852029837426671850f,
|
||||
0.0733464814110803057340336152531165181193f,
|
||||
0.0733464814110803057340336152531165181193f,
|
||||
0.0592985849154367807463677585001085845412f,
|
||||
0.0592985849154367807463677585001085845412f,
|
||||
0.0442774388174198061686027482113382288593f,
|
||||
0.0442774388174198061686027482113382288593f,
|
||||
0.0285313886289336631813078159518782864491f,
|
||||
0.0285313886289336631813078159518782864491f,
|
||||
0.0123412297999871995468056670700372915759f,
|
||||
0.0123412297999871995468056670700372915759f
|
||||
};
|
||||
|
||||
static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), "");
|
||||
|
||||
auto arc = [p0, p1, p2, p3](float t)
|
||||
{
|
||||
const auto p = ImCubicBezierDt(p0, p1, p2, p3, t);
|
||||
const auto l = ImLength(p);
|
||||
return l;
|
||||
};
|
||||
|
||||
const auto z = 0.5f;
|
||||
const auto n = sizeof(t_values) / sizeof(*t_values);
|
||||
|
||||
auto accumulator = 0.0f;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
{
|
||||
const auto t = z * t_values[i] + z;
|
||||
accumulator += c_values[i] * arc(t);
|
||||
}
|
||||
|
||||
return z * accumulator;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve)
|
||||
{
|
||||
return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t)
|
||||
{
|
||||
const auto z1 = t;
|
||||
const auto z2 = z1 * z1;
|
||||
const auto z3 = z1 * z1 * z1;
|
||||
const auto s1 = z1 - 1;
|
||||
const auto s2 = s1 * s1;
|
||||
const auto s3 = s1 * s1 * s1;
|
||||
|
||||
return ImCubicBezierSplitResultT<T>
|
||||
{
|
||||
ImCubicBezierPointsT<T>
|
||||
{
|
||||
p0,
|
||||
z1 * p1 - s1 * p0,
|
||||
z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0,
|
||||
z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0
|
||||
},
|
||||
ImCubicBezierPointsT<T>
|
||||
{
|
||||
z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3,
|
||||
z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3,
|
||||
z1 * p2 - s1 * p3,
|
||||
p3,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t)
|
||||
{
|
||||
return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t);
|
||||
}
|
||||
|
||||
inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3)
|
||||
{
|
||||
auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0;
|
||||
auto b = 6 * p0 - 12 * p1 + 6 * p2;
|
||||
auto c = 3 * p1 - 3 * p0;
|
||||
auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c);
|
||||
|
||||
auto tl = ImMin(p0, p3);
|
||||
auto rb = ImMax(p0, p3);
|
||||
|
||||
# define IM_VEC2_INDEX(v, i) *(&v.x + i)
|
||||
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
if (IM_VEC2_INDEX(delta_squared, i) >= 0)
|
||||
{
|
||||
auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i));
|
||||
|
||||
auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i));
|
||||
if (t0 > 0 && t0 < 1)
|
||||
{
|
||||
auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0);
|
||||
IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
|
||||
IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
|
||||
}
|
||||
|
||||
auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i));
|
||||
if (t1 > 0 && t1 < 1)
|
||||
{
|
||||
auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1);
|
||||
IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
|
||||
IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# undef IM_VEC2_INDEX
|
||||
|
||||
return ImRect(tl, rb);
|
||||
}
|
||||
|
||||
inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve)
|
||||
{
|
||||
return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3);
|
||||
}
|
||||
|
||||
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions)
|
||||
{
|
||||
// http://pomax.github.io/bezierinfo/#projections
|
||||
|
||||
const float epsilon = 1e-5f;
|
||||
const float fixed_step = 1.0f / static_cast<float>(subdivisions - 1);
|
||||
|
||||
ImProjectResult result;
|
||||
result.Point = point;
|
||||
result.Time = 0.0f;
|
||||
result.Distance = FLT_MAX;
|
||||
|
||||
// Step 1: Coarse check
|
||||
for (int i = 0; i < subdivisions; ++i)
|
||||
{
|
||||
auto t = i * fixed_step;
|
||||
auto p = ImCubicBezier(p0, p1, p2, p3, t);
|
||||
auto s = point - p;
|
||||
auto d = ImDot(s, s);
|
||||
|
||||
if (d < result.Distance)
|
||||
{
|
||||
result.Point = p;
|
||||
result.Time = t;
|
||||
result.Distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon)
|
||||
{
|
||||
result.Distance = ImSqrt(result.Distance);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Step 2: Fine check
|
||||
auto left = result.Time - fixed_step;
|
||||
auto right = result.Time + fixed_step;
|
||||
auto step = fixed_step * 0.1f;
|
||||
|
||||
for (auto t = left; t < right + step; t += step)
|
||||
{
|
||||
auto p = ImCubicBezier(p0, p1, p2, p3, t);
|
||||
auto s = point - p;
|
||||
auto d = ImDot(s, s);
|
||||
|
||||
if (d < result.Distance)
|
||||
{
|
||||
result.Point = p;
|
||||
result.Time = t;
|
||||
result.Distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
result.Distance = ImSqrt(result.Distance);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions)
|
||||
{
|
||||
return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions);
|
||||
}
|
||||
|
||||
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1)
|
||||
{
|
||||
auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; };
|
||||
|
||||
auto A = b / a;
|
||||
auto B = c / a;
|
||||
auto C = d / a;
|
||||
|
||||
auto Q = (3 * B - ImPow(A, 2)) / 9;
|
||||
auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54;
|
||||
auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant
|
||||
|
||||
if (D >= 0) // complex or duplicate roots
|
||||
{
|
||||
auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f));
|
||||
auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f));
|
||||
|
||||
roots[0] = -A / 3 + (S + T); // real root
|
||||
roots[1] = -A / 3 - (S + T) / 2; // real part of complex root
|
||||
roots[2] = -A / 3 - (S + T) / 2; // real part of complex root
|
||||
auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair
|
||||
|
||||
// discard complex roots
|
||||
if (Im != 0)
|
||||
count = 1;
|
||||
else
|
||||
count = 3;
|
||||
}
|
||||
else // distinct real roots
|
||||
{
|
||||
auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3)));
|
||||
|
||||
roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3;
|
||||
roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3;
|
||||
roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3;
|
||||
|
||||
count = 3;
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
// https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as
|
||||
//
|
||||
// Start with Bezier using Bernstein polynomials for weighting functions:
|
||||
// (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3
|
||||
//
|
||||
// Expand and collect terms to form linear combinations of original Bezier
|
||||
// controls. This ends up with a vector cubic in t:
|
||||
// (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0
|
||||
// /\ /\ /\ /\
|
||||
// || || || ||
|
||||
// c3 c2 c1 c0
|
||||
|
||||
// Calculate the coefficients
|
||||
auto c3 = -p0 + 3 * p1 - 3 * p2 + p3;
|
||||
auto c2 = 3 * p0 - 6 * p1 + 3 * p2;
|
||||
auto c1 = -3 * p0 + 3 * p1;
|
||||
auto c0 = p0;
|
||||
|
||||
// Convert line to normal form: ax + by + c = 0
|
||||
auto a = a1.y - a0.y;
|
||||
auto b = a0.x - a1.x;
|
||||
auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x);
|
||||
|
||||
// Rotate each cubic coefficient using line for new coordinate system?
|
||||
// Find roots of rotated cubic
|
||||
float roots[3];
|
||||
auto rootCount = cubic_roots(
|
||||
a * c3.x + b * c3.y,
|
||||
a * c2.x + b * c2.y,
|
||||
a * c1.x + b * c1.y,
|
||||
a * c0.x + b * c0.y + c,
|
||||
roots);
|
||||
|
||||
// Any roots in closed interval [0,1] are intersections on Bezier, but
|
||||
// might not be on the line segment.
|
||||
// Find intersections and calculate point coordinates
|
||||
|
||||
auto min = ImMin(a0, a1);
|
||||
auto max = ImMax(a0, a1);
|
||||
|
||||
ImCubicBezierIntersectResult result;
|
||||
auto points = result.Points;
|
||||
|
||||
for (int i = 0; i < rootCount; ++i)
|
||||
{
|
||||
auto root = roots[i];
|
||||
|
||||
if (0 <= root && root <= 1)
|
||||
{
|
||||
// We're within the Bezier curve
|
||||
// Find point on Bezier
|
||||
auto p = ImCubicBezier(p0, p1, p2, p3, root);
|
||||
|
||||
// See if point is on line segment
|
||||
// Had to make special cases for vertical and horizontal lines due
|
||||
// to slight errors in calculation of p00
|
||||
if (a0.x == a1.x)
|
||||
{
|
||||
if (min.y <= p.y && p.y <= max.y)
|
||||
*points++ = p;
|
||||
}
|
||||
else if (a0.y == a1.y)
|
||||
{
|
||||
if (min.x <= p.x && p.x <= max.x)
|
||||
*points++ = p;
|
||||
}
|
||||
else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y)
|
||||
{
|
||||
*points++ = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.Count = static_cast<int>(points - result.Points);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line)
|
||||
{
|
||||
return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B);
|
||||
}
|
||||
|
||||
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
|
||||
{
|
||||
return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
|
||||
}
|
||||
|
||||
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
|
||||
{
|
||||
struct Tesselator
|
||||
{
|
||||
ImCubicBezierSubdivideCallback Callback;
|
||||
void* UserPointer;
|
||||
float TesselationTollerance;
|
||||
ImCubicBezierSubdivideFlags Flags;
|
||||
|
||||
void Commit(const ImVec2& p, const ImVec2& t)
|
||||
{
|
||||
ImCubicBezierSubdivideSample sample;
|
||||
sample.Point = p;
|
||||
sample.Tangent = t;
|
||||
Callback(sample, UserPointer);
|
||||
}
|
||||
|
||||
void Subdivide(const ImCubicBezierPoints& curve, int level = 0)
|
||||
{
|
||||
float dx = curve.P3.x - curve.P0.x;
|
||||
float dy = curve.P3.y - curve.P0.y;
|
||||
float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx);
|
||||
float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx);
|
||||
d2 = (d2 >= 0) ? d2 : -d2;
|
||||
d3 = (d3 >= 0) ? d3 : -d3;
|
||||
if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy))
|
||||
{
|
||||
Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f));
|
||||
}
|
||||
else if (level < 10)
|
||||
{
|
||||
const auto p12 = (curve.P0 + curve.P1) * 0.5f;
|
||||
const auto p23 = (curve.P1 + curve.P2) * 0.5f;
|
||||
const auto p34 = (curve.P2 + curve.P3) * 0.5f;
|
||||
const auto p123 = (p12 + p23) * 0.5f;
|
||||
const auto p234 = (p23 + p34) * 0.5f;
|
||||
const auto p1234 = (p123 + p234) * 0.5f;
|
||||
|
||||
Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1);
|
||||
Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (tess_tol < 0)
|
||||
tess_tol = 1.118f; // sqrtf(1.25f)
|
||||
|
||||
Tesselator tesselator;
|
||||
tesselator.Callback = callback;
|
||||
tesselator.UserPointer = user_pointer;
|
||||
tesselator.TesselationTollerance = tess_tol * tess_tol;
|
||||
tesselator.Flags = flags;
|
||||
|
||||
if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst))
|
||||
tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f));
|
||||
|
||||
tesselator.Subdivide(curve, 0);
|
||||
}
|
||||
|
||||
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
|
||||
{
|
||||
auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
|
||||
{
|
||||
auto& callback = *reinterpret_cast<F*>(user_pointer);
|
||||
callback(p);
|
||||
};
|
||||
|
||||
ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
|
||||
}
|
||||
|
||||
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
|
||||
{
|
||||
auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
|
||||
{
|
||||
auto& callback = *reinterpret_cast<F*>(user_pointer);
|
||||
callback(p);
|
||||
};
|
||||
|
||||
ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags);
|
||||
}
|
||||
|
||||
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
|
||||
{
|
||||
if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0)
|
||||
return;
|
||||
|
||||
ImCubicBezierFixedStepSample sample;
|
||||
sample.T = 0.0f;
|
||||
sample.Length = 0.0f;
|
||||
sample.Point = p0;
|
||||
sample.BreakSearch = false;
|
||||
|
||||
callback(sample, user_pointer);
|
||||
if (sample.BreakSearch)
|
||||
return;
|
||||
|
||||
const auto length = ImCubicBezierLength(p0, p1, p2, p3);
|
||||
const auto point_count = static_cast<int>(length / step) + (overshoot ? 2 : 1);
|
||||
const auto t_min = 0.0f;
|
||||
const auto t_max = step * point_count / length;
|
||||
const auto t_0 = (t_min + t_max) * 0.5f;
|
||||
|
||||
// #todo: replace map with ImVector + binary search
|
||||
std::map<float, float> cache;
|
||||
for (int point_index = 1; point_index < point_count; ++point_index)
|
||||
{
|
||||
const auto targetLength = point_index * step;
|
||||
|
||||
float t_start = t_min;
|
||||
float t_end = t_max;
|
||||
float t = t_0;
|
||||
|
||||
float t_best = t;
|
||||
float error_best = length;
|
||||
|
||||
while (true)
|
||||
{
|
||||
auto cacheIt = cache.find(t);
|
||||
if (cacheIt == cache.end())
|
||||
{
|
||||
const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left;
|
||||
const auto length = ImCubicBezierLength(front);
|
||||
|
||||
cacheIt = cache.emplace(t, length).first;
|
||||
}
|
||||
|
||||
const auto length = cacheIt->second;
|
||||
const auto error = targetLength - length;
|
||||
|
||||
if (error < error_best)
|
||||
{
|
||||
error_best = error;
|
||||
t_best = t;
|
||||
}
|
||||
|
||||
if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error)
|
||||
{
|
||||
sample.T = t;
|
||||
sample.Length = length;
|
||||
sample.Point = ImCubicBezier(p0, p1, p2, p3, t);
|
||||
|
||||
callback(sample, user_pointer);
|
||||
if (sample.BreakSearch)
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
else if (error < 0.0f)
|
||||
t_end = t;
|
||||
else // if (error > 0.0f)
|
||||
t_start = t;
|
||||
|
||||
t = (t_start + t_end) * 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
|
||||
{
|
||||
ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
|
||||
}
|
||||
|
||||
// F has signature void(const ImCubicBezierFixedStepSample& p)
|
||||
template <typename F>
|
||||
inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
|
||||
{
|
||||
auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
|
||||
{
|
||||
auto& callback = *reinterpret_cast<F*>(user_pointer);
|
||||
callback(sample);
|
||||
};
|
||||
|
||||
ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
|
||||
{
|
||||
auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
|
||||
{
|
||||
auto& callback = *reinterpret_cast<F*>(user_pointer);
|
||||
callback(sample);
|
||||
};
|
||||
|
||||
ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // __IMGUI_BEZIER_MATH_INL__
|
||||
@@ -0,0 +1,526 @@
|
||||
# define IMGUI_DEFINE_MATH_OPERATORS
|
||||
# include "imgui_canvas.h"
|
||||
# include <type_traits>
|
||||
|
||||
// https://stackoverflow.com/a/36079786
|
||||
# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \
|
||||
\
|
||||
template <typename __boost_has_member_T__> \
|
||||
class __trait_name__ \
|
||||
{ \
|
||||
using check_type = ::std::remove_const_t<__boost_has_member_T__>; \
|
||||
struct no_type {char x[2];}; \
|
||||
using yes_type = char; \
|
||||
\
|
||||
struct base { void __member_name__() {}}; \
|
||||
struct mixin : public base, public check_type {}; \
|
||||
\
|
||||
template <void (base::*)()> struct aux {}; \
|
||||
\
|
||||
template <typename U> static no_type test(aux<&U::__member_name__>*); \
|
||||
template <typename U> static yes_type test(...); \
|
||||
\
|
||||
public: \
|
||||
\
|
||||
static constexpr bool value = (sizeof(yes_type) == sizeof(test<mixin>(0))); \
|
||||
}
|
||||
|
||||
namespace ImCanvasDetails {
|
||||
|
||||
DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale);
|
||||
|
||||
struct FringeScaleRef
|
||||
{
|
||||
// Overload is present when ImDrawList does have _FringeScale member variable.
|
||||
template <typename T>
|
||||
static float& Get(typename std::enable_if<HasFringeScale<T>::value, T>::type* drawList)
|
||||
{
|
||||
return drawList->_FringeScale;
|
||||
}
|
||||
|
||||
// Overload is present when ImDrawList does not have _FringeScale member variable.
|
||||
template <typename T>
|
||||
static float& Get(typename std::enable_if<!HasFringeScale<T>::value, T>::type*)
|
||||
{
|
||||
static float placeholder = 1.0f;
|
||||
return placeholder;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ImCanvasDetails
|
||||
|
||||
// Returns a reference to _FringeScale extension to ImDrawList
|
||||
//
|
||||
// If ImDrawList does not have _FringeScale a placeholder is returned.
|
||||
static inline float& ImFringeScaleRef(ImDrawList* drawList)
|
||||
{
|
||||
using namespace ImCanvasDetails;
|
||||
return FringeScaleRef::Get<ImDrawList>(drawList);
|
||||
}
|
||||
|
||||
static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); }
|
||||
|
||||
bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size)
|
||||
{
|
||||
return Begin(ImGui::GetID(id), size);
|
||||
}
|
||||
|
||||
bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size)
|
||||
{
|
||||
IM_ASSERT(m_InBeginEnd == false);
|
||||
|
||||
m_WidgetPosition = ImGui::GetCursorScreenPos();
|
||||
m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail());
|
||||
m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize);
|
||||
m_DrawList = ImGui::GetWindowDrawList();
|
||||
|
||||
UpdateViewTransformPosition();
|
||||
|
||||
if (ImGui::IsClippedEx(m_WidgetRect, id)) // IsClippedEx dropped its 3rd arg upstream in ImGui
|
||||
return false;
|
||||
|
||||
// Save current channel, so we can assert when user
|
||||
// call canvas API with different one.
|
||||
m_ExpectedChannel = m_DrawList->_Splitter._Current;
|
||||
|
||||
// #debug: Canvas content.
|
||||
//m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, IM_COL32(0, 0, 0, 64));
|
||||
m_DrawList->AddRect(m_WidgetRect.Min, m_WidgetRect.Max, IM_COL32(255, 0, 255, 64));
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
|
||||
|
||||
# if IMGUI_EX_CANVAS_DEFERED()
|
||||
m_Ranges.resize(0);
|
||||
# endif
|
||||
|
||||
SaveInputState();
|
||||
SaveViewportState();
|
||||
|
||||
EnterLocalSpace();
|
||||
|
||||
// Emit dummy widget matching bounds of the canvas.
|
||||
ImGui::SetCursorScreenPos(m_ViewRect.Min);
|
||||
ImGui::Dummy(m_ViewRect.GetSize());
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
|
||||
|
||||
m_InBeginEnd = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::End()
|
||||
{
|
||||
// If you're here your call to Begin() returned false,
|
||||
// or Begin() wasn't called at all.
|
||||
IM_ASSERT(m_InBeginEnd == true);
|
||||
|
||||
// If you're here, please make sure you do not interleave
|
||||
// channel splitter with canvas.
|
||||
// Always call canvas function with using same channel.
|
||||
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
|
||||
|
||||
//auto& io = ImGui::GetIO();
|
||||
|
||||
// Check: Unmatched calls to Suspend() / Resume(). Please check your code.
|
||||
IM_ASSERT(m_SuspendCounter == 0);
|
||||
|
||||
LeaveLocalSpace();
|
||||
|
||||
// Emit dummy widget matching bounds of the canvas.
|
||||
ImGui::SetCursorScreenPos(m_WidgetPosition);
|
||||
ImGui::Dummy(m_WidgetSize);
|
||||
|
||||
// #debug: Rect around canvas. Content should be inside these bounds.
|
||||
//m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255));
|
||||
|
||||
m_InBeginEnd = false;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale)
|
||||
{
|
||||
SetView(CanvasView(origin, scale));
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::SetView(const CanvasView& view)
|
||||
{
|
||||
if (m_InBeginEnd)
|
||||
LeaveLocalSpace();
|
||||
|
||||
if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y)
|
||||
{
|
||||
m_View.Origin = view.Origin;
|
||||
|
||||
UpdateViewTransformPosition();
|
||||
}
|
||||
|
||||
if (m_View.Scale != view.Scale)
|
||||
{
|
||||
m_View.Scale = view.Scale;
|
||||
m_View.InvScale = view.InvScale;
|
||||
}
|
||||
|
||||
if (m_InBeginEnd)
|
||||
EnterLocalSpace();
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint)
|
||||
{
|
||||
auto view = CalcCenterView(canvasPoint);
|
||||
SetView(view);
|
||||
}
|
||||
|
||||
ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImVec2& canvasPoint) const
|
||||
{
|
||||
auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f);
|
||||
auto localOffset = canvasPoint - localCenter;
|
||||
auto offset = FromLocalV(localOffset);
|
||||
|
||||
return CanvasView{ m_View.Origin - offset, m_View.Scale };
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect)
|
||||
{
|
||||
auto view = CalcCenterView(canvasRect);
|
||||
|
||||
SetView(view);
|
||||
}
|
||||
|
||||
ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImRect& canvasRect) const
|
||||
{
|
||||
auto canvasRectSize = canvasRect.GetSize();
|
||||
|
||||
if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f)
|
||||
return View();
|
||||
|
||||
auto widgetAspectRatio = m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f;
|
||||
auto canvasRectAspectRatio = canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f;
|
||||
|
||||
if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f)
|
||||
return View();
|
||||
|
||||
auto newOrigin = m_View.Origin;
|
||||
auto newScale = m_View.Scale;
|
||||
if (canvasRectAspectRatio > widgetAspectRatio)
|
||||
{
|
||||
// width span across view
|
||||
newScale = m_WidgetSize.x / canvasRectSize.x;
|
||||
newOrigin = canvasRect.Min * -newScale;
|
||||
newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// height span across view
|
||||
newScale = m_WidgetSize.y / canvasRectSize.y;
|
||||
newOrigin = canvasRect.Min * -newScale;
|
||||
newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f;
|
||||
}
|
||||
|
||||
return CanvasView{ newOrigin, newScale };
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::Suspend()
|
||||
{
|
||||
// If you're here, please make sure you do not interleave
|
||||
// channel splitter with canvas.
|
||||
// Always call canvas function with using same channel.
|
||||
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
|
||||
|
||||
if (m_SuspendCounter == 0)
|
||||
LeaveLocalSpace();
|
||||
|
||||
++m_SuspendCounter;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::Resume()
|
||||
{
|
||||
// If you're here, please make sure you do not interleave
|
||||
// channel splitter with canvas.
|
||||
// Always call canvas function with using same channel.
|
||||
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
|
||||
|
||||
// Check: Number of calls to Resume() do not match calls to Suspend(). Please check your code.
|
||||
IM_ASSERT(m_SuspendCounter > 0);
|
||||
if (--m_SuspendCounter == 0)
|
||||
EnterLocalSpace();
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const
|
||||
{
|
||||
return point * m_View.Scale + m_ViewTransformPosition;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, const CanvasView& view) const
|
||||
{
|
||||
return point * view.Scale + view.Origin + m_WidgetPosition;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const
|
||||
{
|
||||
return vector * m_View.Scale;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, const CanvasView& view) const
|
||||
{
|
||||
return vector * view.Scale;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const
|
||||
{
|
||||
return (point - m_ViewTransformPosition) * m_View.InvScale;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, const CanvasView& view) const
|
||||
{
|
||||
return (point - view.Origin - m_WidgetPosition) * view.InvScale;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const
|
||||
{
|
||||
return vector * m_View.InvScale;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, const CanvasView& view) const
|
||||
{
|
||||
return vector * view.InvScale;
|
||||
}
|
||||
|
||||
ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const
|
||||
{
|
||||
ImRect result;
|
||||
result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale;
|
||||
result.Max = (m_WidgetSize - view.Origin) * view.InvScale;
|
||||
return result;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::UpdateViewTransformPosition()
|
||||
{
|
||||
m_ViewTransformPosition = m_View.Origin + m_WidgetPosition;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::SaveInputState()
|
||||
{
|
||||
auto& io = ImGui::GetIO();
|
||||
m_MousePosBackup = io.MousePos;
|
||||
m_MousePosPrevBackup = io.MousePosPrev;
|
||||
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
|
||||
m_MouseClickedPosBackup[i] = io.MouseClickedPos[i];
|
||||
|
||||
// Record cursor max to prevent scrollbars from appearing.
|
||||
m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::RestoreInputState()
|
||||
{
|
||||
auto& io = ImGui::GetIO();
|
||||
io.MousePos = m_MousePosBackup;
|
||||
io.MousePosPrev = m_MousePosPrevBackup;
|
||||
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
|
||||
io.MouseClickedPos[i] = m_MouseClickedPosBackup[i];
|
||||
ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
|
||||
}
|
||||
|
||||
// UsdLayerManager local patch, ported from upstream imgui-node-editor
|
||||
// (see the declaration in imgui_canvas.h for why).
|
||||
void ImGuiEx::Canvas::SaveViewportState()
|
||||
{
|
||||
# if defined(IMGUI_HAS_VIEWPORT)
|
||||
auto window = ImGui::GetCurrentWindow();
|
||||
auto viewport = ImGui::GetWindowViewport();
|
||||
|
||||
m_WindowPosBackup = window->Pos;
|
||||
m_ViewportPosBackup = viewport->Pos;
|
||||
m_ViewportSizeBackup = viewport->Size;
|
||||
# if IMGUI_VERSION_NUM > 18002
|
||||
m_ViewportWorkPosBackup = viewport->WorkPos;
|
||||
m_ViewportWorkSizeBackup = viewport->WorkSize;
|
||||
# else
|
||||
m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin;
|
||||
m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax;
|
||||
# endif
|
||||
# endif
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::RestoreViewportState()
|
||||
{
|
||||
# if defined(IMGUI_HAS_VIEWPORT)
|
||||
auto window = ImGui::GetCurrentWindow();
|
||||
auto viewport = ImGui::GetWindowViewport();
|
||||
|
||||
window->Pos = m_WindowPosBackup;
|
||||
viewport->Pos = m_ViewportPosBackup;
|
||||
viewport->Size = m_ViewportSizeBackup;
|
||||
# if IMGUI_VERSION_NUM > 18002
|
||||
viewport->WorkPos = m_ViewportWorkPosBackup;
|
||||
viewport->WorkSize = m_ViewportWorkSizeBackup;
|
||||
# else
|
||||
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup;
|
||||
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup;
|
||||
# endif
|
||||
# endif
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::EnterLocalSpace()
|
||||
{
|
||||
// Prepare ImDrawList for drawing in local coordinate system:
|
||||
// - determine visible part of the canvas
|
||||
// - start unique draw command
|
||||
// - add clip rect matching canvas size
|
||||
// - record current command index
|
||||
// - record current vertex write index
|
||||
|
||||
// Determine visible part of the canvas. Make it before
|
||||
// adding new command, to avoid round rip where command
|
||||
// is removed in PopClipRect() and added again next PushClipRect().
|
||||
ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true);
|
||||
auto clipped_clip_rect = m_DrawList->_ClipRectStack.back();
|
||||
ImGui::PopClipRect();
|
||||
|
||||
// Make sure we do not share draw command with anyone. We don't want to mess
|
||||
// with someones clip rectangle.
|
||||
|
||||
// #FIXME:
|
||||
// This condition is not enough to avoid when user choose
|
||||
// to use channel splitter.
|
||||
//
|
||||
// To deal with Suspend()/Resume() calls empty draw command
|
||||
// is always added then splitter is active. Otherwise
|
||||
// channel merger will collapse our draw command one with
|
||||
// different clip rectangle.
|
||||
//
|
||||
// More investigation is needed. To get to the bottom of this.
|
||||
if ((!m_DrawList->CmdBuffer.empty() && m_DrawList->CmdBuffer.back().ElemCount > 0) || m_DrawList->_Splitter._Count > 1)
|
||||
m_DrawList->AddDrawCmd();
|
||||
|
||||
# if IMGUI_EX_CANVAS_DEFERED()
|
||||
m_Ranges.resize(m_Ranges.Size + 1);
|
||||
m_CurrentRange = &m_Ranges.back();
|
||||
m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
|
||||
m_CurrentRange->BeginVertexIndex = m_DrawList->_VtxCurrentIdx;
|
||||
# endif
|
||||
m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
|
||||
m_DrawListStartVertexIndex = m_DrawList->_VtxCurrentIdx;
|
||||
|
||||
// Clip rectangle in parent canvas space and move it to local space.
|
||||
clipped_clip_rect.x = (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||
clipped_clip_rect.y = (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||
clipped_clip_rect.z = (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||
clipped_clip_rect.w = (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||
ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false);
|
||||
|
||||
// Transform mouse position to local space.
|
||||
auto& io = ImGui::GetIO();
|
||||
io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale;
|
||||
io.MousePosPrev = (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale;
|
||||
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
|
||||
io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
|
||||
|
||||
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
|
||||
// move the viewport rect into local space too, so IsMouseHoveringRect's
|
||||
// screen-space viewport overlap check (docking-branch ImGui) passes for
|
||||
// items at canvas coordinates outside the screen range (e.g. negative).
|
||||
// See SaveViewportState() declaration for the full story.
|
||||
# if defined(IMGUI_HAS_VIEWPORT)
|
||||
{
|
||||
auto window = ImGui::GetCurrentWindow();
|
||||
window->Pos = ImVec2(0.0f, 0.0f);
|
||||
|
||||
auto viewport_min = m_ViewportPosBackup;
|
||||
auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup;
|
||||
|
||||
viewport_min.x = (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||
viewport_min.y = (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||
viewport_max.x = (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||
viewport_max.y = (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||
|
||||
auto viewport = ImGui::GetWindowViewport();
|
||||
viewport->Pos = viewport_min;
|
||||
viewport->Size = viewport_max - viewport_min;
|
||||
|
||||
# if IMGUI_VERSION_NUM > 18002
|
||||
viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale;
|
||||
viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale;
|
||||
# else
|
||||
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale;
|
||||
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale;
|
||||
# endif
|
||||
}
|
||||
# endif
|
||||
|
||||
m_ViewRect = CalcViewRect(m_View);;
|
||||
|
||||
auto& fringeScale = ImFringeScaleRef(m_DrawList);
|
||||
m_LastFringeScale = fringeScale;
|
||||
fringeScale *= m_View.InvScale;
|
||||
}
|
||||
|
||||
void ImGuiEx::Canvas::LeaveLocalSpace()
|
||||
{
|
||||
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
|
||||
|
||||
# if IMGUI_EX_CANVAS_DEFERED()
|
||||
IM_ASSERT(m_CurrentRange != nullptr);
|
||||
|
||||
m_CurrentRange->EndVertexIndex = m_DrawList->_VtxCurrentIdx;
|
||||
m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size();
|
||||
if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex)
|
||||
{
|
||||
// Drop empty range
|
||||
m_Ranges.resize(m_Ranges.Size - 1);
|
||||
}
|
||||
m_CurrentRange = nullptr;
|
||||
# endif
|
||||
|
||||
// Move vertices to screen space.
|
||||
auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex;
|
||||
auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx;
|
||||
|
||||
// If canvas view is not scaled take a faster path.
|
||||
if (m_View.Scale != 1.0f)
|
||||
{
|
||||
while (vertex < vertexEnd)
|
||||
{
|
||||
vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x;
|
||||
vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y;
|
||||
++vertex;
|
||||
}
|
||||
|
||||
// Move clip rectangles to screen space.
|
||||
for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
|
||||
{
|
||||
auto& command = m_DrawList->CmdBuffer[i];
|
||||
command.ClipRect.x = command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x;
|
||||
command.ClipRect.y = command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y;
|
||||
command.ClipRect.z = command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x;
|
||||
command.ClipRect.w = command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (vertex < vertexEnd)
|
||||
{
|
||||
vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x;
|
||||
vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y;
|
||||
++vertex;
|
||||
}
|
||||
|
||||
// Move clip rectangles to screen space.
|
||||
for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
|
||||
{
|
||||
auto& command = m_DrawList->CmdBuffer[i];
|
||||
command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x;
|
||||
command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y;
|
||||
command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x;
|
||||
command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y;
|
||||
}
|
||||
}
|
||||
|
||||
auto& fringeScale = ImFringeScaleRef(m_DrawList);
|
||||
fringeScale = m_LastFringeScale;
|
||||
|
||||
// And pop \o/
|
||||
ImGui::PopClipRect();
|
||||
|
||||
RestoreInputState();
|
||||
RestoreViewportState();
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Canvas widget - view over infinite virtual space.
|
||||
//
|
||||
// Canvas allows you to draw your widgets anywhere over infinite space and provide
|
||||
// view over it with support for panning and scaling.
|
||||
//
|
||||
// When you enter a canvas ImGui is moved to virtual space which mean:
|
||||
// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top left corner
|
||||
// of the canvas on the screen (this can be changed usign CanvasView()).
|
||||
// - Mouse input is brought to canvas space, so widgets works as usual.
|
||||
// - Everything you draw with ImDrawList will be in virtual space.
|
||||
//
|
||||
// By default origin point is on top left corner of canvas widget. It can be
|
||||
// changed with call to CanvasView() where you can specify what part of space
|
||||
// should be viewed by setting viewport origin point and scale. Current state
|
||||
// can be queried with CanvasViewOrigin() and CanvasViewScale().
|
||||
//
|
||||
// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can query
|
||||
// it using CanvasContentMin/Max/Size functions. They are useful if you to not specify
|
||||
// canvas size in which case all free space is used.
|
||||
//
|
||||
// Bounds of visible region of infinite space can be queried using CanvasViewMin/Max/Size
|
||||
// functions. Everything that is drawn outside of this region will be clipped
|
||||
// as usual in ImGui.
|
||||
//
|
||||
// While drawing inside canvas you can translate position from world (usual ImGui space)
|
||||
// to virtual space and back usign CanvasFromWorld()/CanvasToWorld().
|
||||
//
|
||||
// Canvas can be nested in each other (they are regular widgets after all). There
|
||||
// is a way to transform position between current and parent canvas with
|
||||
// CanvasFromParent()/CanvasToParent().
|
||||
//
|
||||
// Sometimes in more elaborate scenarios you want to move out canvas virtual space,
|
||||
// do something and came back. You can do that with SuspendCanvas() and ResumeCanvas().
|
||||
//
|
||||
// Note:
|
||||
// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() scope.
|
||||
//
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
# ifndef __IMGUI_EX_CANVAS_H__
|
||||
# define __IMGUI_EX_CANVAS_H__
|
||||
# pragma once
|
||||
|
||||
# include <imgui.h>
|
||||
# include <imgui_internal.h> // ImRect, ImFloor
|
||||
|
||||
namespace ImGuiEx {
|
||||
|
||||
struct CanvasView
|
||||
{
|
||||
ImVec2 Origin;
|
||||
float Scale = 1.0f;
|
||||
float InvScale = 1.0f;
|
||||
|
||||
CanvasView() = default;
|
||||
CanvasView(const ImVec2& origin, float scale)
|
||||
: Origin(origin)
|
||||
, Scale(scale)
|
||||
, InvScale(scale ? 1.0f / scale : 0.0f)
|
||||
{
|
||||
}
|
||||
|
||||
void Set(const ImVec2& origin, float scale)
|
||||
{
|
||||
*this = CanvasView(origin, scale);
|
||||
}
|
||||
};
|
||||
|
||||
// Canvas widget represent view over infinite plane.
|
||||
//
|
||||
// It acts like a child window without scroll bars with
|
||||
// ability to zoom to specific part of canvas plane.
|
||||
//
|
||||
// Widgets are clipped according to current view exactly
|
||||
// same way ImGui do. To avoid `missing widgets` artifacts first
|
||||
// setup visible region with SetView() then draw content.
|
||||
//
|
||||
// Everything drawn with ImDrawList betwen calls to Begin()/End()
|
||||
// will be drawn on canvas plane. This behavior can be suspended
|
||||
// by calling Suspend() and resumed by calling Resume().
|
||||
//
|
||||
// Warning:
|
||||
// Please do not interleave canvas with use of channel splitter.
|
||||
// Keep channel splitter contained inside canvas or always
|
||||
// call canvas functions from same channel.
|
||||
struct Canvas
|
||||
{
|
||||
// Begins drawing content of canvas plane.
|
||||
//
|
||||
// When false is returned that mean canvas is not visible to the
|
||||
// user can drawing should be skipped and End() not called.
|
||||
// When true is returned drawing must be ended with call to End().
|
||||
//
|
||||
// If any size component is equal to zero or less canvas will
|
||||
// automatically expand to all available area on that axis.
|
||||
// So (0, 300) will take horizontal space and have height
|
||||
// of 300 points. (0, 0) will take all remaining space of
|
||||
// the window.
|
||||
//
|
||||
// You can query size of the canvas while it is being drawn
|
||||
// by calling Rect().
|
||||
bool Begin(const char* id, const ImVec2& size);
|
||||
bool Begin(ImGuiID id, const ImVec2& size);
|
||||
|
||||
// Ends interaction with canvas plane.
|
||||
//
|
||||
// Must be called only when Begin() retuned true.
|
||||
void End();
|
||||
|
||||
// Sets visible region of canvas plane.
|
||||
//
|
||||
// Origin is an offset of infinite plane origin from top left
|
||||
// corner of the canvas.
|
||||
//
|
||||
// Scale greater than 1 make canvas content be bigger, less than 1 smaller.
|
||||
void SetView(const ImVec2& origin, float scale);
|
||||
void SetView(const CanvasView& view);
|
||||
|
||||
// Centers view over specific point on canvas plane.
|
||||
//
|
||||
// View will be centered on specific point by changing origin
|
||||
// but not scale.
|
||||
void CenterView(const ImVec2& canvasPoint);
|
||||
|
||||
// Calculates view over specific point on canvas plane.
|
||||
CanvasView CalcCenterView(const ImVec2& canvasPoint) const;
|
||||
|
||||
// Centers view over specific rectangle on canvas plane.
|
||||
//
|
||||
// Whole rectangle will fit in canvas view. This will affect both
|
||||
// origin and scale.
|
||||
void CenterView(const ImRect& canvasRect);
|
||||
|
||||
// Calculates view over specific rectangle on canvas plane.
|
||||
CanvasView CalcCenterView(const ImRect& canvasRect) const;
|
||||
|
||||
// Suspends canvas by returning to normal ImGui transformation space.
|
||||
// While suspended UI will not be drawn on canvas plane.
|
||||
//
|
||||
// Calls to Suspend()/Resume() are symetrical. Each call to Suspend()
|
||||
// must be matched with call to Resume().
|
||||
void Suspend();
|
||||
void Resume();
|
||||
|
||||
// Transforms point from canvas plane to ImGui.
|
||||
ImVec2 FromLocal(const ImVec2& point) const;
|
||||
ImVec2 FromLocal(const ImVec2& point, const CanvasView& view) const;
|
||||
|
||||
// Transforms vector from canvas plant to ImGui.
|
||||
ImVec2 FromLocalV(const ImVec2& vector) const;
|
||||
ImVec2 FromLocalV(const ImVec2& vector, const CanvasView& view) const;
|
||||
|
||||
// Transforms point from ImGui to canvas plane.
|
||||
ImVec2 ToLocal(const ImVec2& point) const;
|
||||
ImVec2 ToLocal(const ImVec2& point, const CanvasView& view) const;
|
||||
|
||||
// Transforms vector from ImGui to canvas plane.
|
||||
ImVec2 ToLocalV(const ImVec2& vector) const;
|
||||
ImVec2 ToLocalV(const ImVec2& vector, const CanvasView& view) const;
|
||||
|
||||
// Returns widget bounds.
|
||||
//
|
||||
// Note:
|
||||
// Rect is valid after call to Begin().
|
||||
const ImRect& Rect() const { return m_WidgetRect; }
|
||||
|
||||
// Returns visible region on canvas plane (in canvas plane coordinates).
|
||||
const ImRect& ViewRect() const { return m_ViewRect; }
|
||||
|
||||
// Calculates visible region for view.
|
||||
ImRect CalcViewRect(const CanvasView& view) const;
|
||||
|
||||
// Returns current view.
|
||||
const CanvasView& View() const { return m_View; }
|
||||
|
||||
// Returns origin of the view.
|
||||
//
|
||||
// Origin is an offset of infinite plane origin from top left
|
||||
// corner of the canvas.
|
||||
const ImVec2& ViewOrigin() const { return m_View.Origin; }
|
||||
|
||||
// Returns scale of the view.
|
||||
float ViewScale() const { return m_View.Scale; }
|
||||
|
||||
// Returns true if canvas is suspended.
|
||||
//
|
||||
// See: Suspend()/Resume()
|
||||
bool IsSuspended() const { return m_SuspendCounter > 0; }
|
||||
|
||||
private:
|
||||
# define IMGUI_EX_CANVAS_DEFERED() 0
|
||||
|
||||
# if IMGUI_EX_CANVAS_DEFERED()
|
||||
struct Range
|
||||
{
|
||||
int BeginVertexIndex = 0;
|
||||
int EndVertexIndex = 0;
|
||||
int BeginComandIndex = 0;
|
||||
int EndCommandIndex = 0;
|
||||
};
|
||||
# endif
|
||||
|
||||
void UpdateViewTransformPosition();
|
||||
|
||||
void SaveInputState();
|
||||
void RestoreInputState();
|
||||
|
||||
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
|
||||
// with a viewport-enabled ImGui (IMGUI_HAS_VIEWPORT, i.e. the docking
|
||||
// branch), ImGui::IsMouseHoveringRect() — and therefore every item's
|
||||
// hover test — requires the rect to overlap the mouse viewport's rect,
|
||||
// which is in *screen* space. Inside the canvas, rects are in canvas
|
||||
// space, so anything at canvas coordinates outside the screen-space
|
||||
// range (e.g. negative) went hover/click-dead. Fix: transform the
|
||||
// viewport rect into canvas-local space while inside EnterLocalSpace()
|
||||
// and restore it on LeaveLocalSpace(), exactly like the mouse position.
|
||||
void SaveViewportState();
|
||||
void RestoreViewportState();
|
||||
|
||||
void EnterLocalSpace();
|
||||
void LeaveLocalSpace();
|
||||
|
||||
bool m_InBeginEnd = false;
|
||||
|
||||
ImVec2 m_WidgetPosition;
|
||||
ImVec2 m_WidgetSize;
|
||||
ImRect m_WidgetRect;
|
||||
|
||||
ImDrawList* m_DrawList = nullptr;
|
||||
int m_ExpectedChannel = 0;
|
||||
|
||||
# if IMGUI_EX_CANVAS_DEFERED()
|
||||
ImVector<Range> m_Ranges;
|
||||
Range* m_CurrentRange = nullptr;
|
||||
# endif
|
||||
|
||||
int m_DrawListCommadBufferSize = 0;
|
||||
int m_DrawListStartVertexIndex = 0;
|
||||
|
||||
CanvasView m_View;
|
||||
ImRect m_ViewRect;
|
||||
|
||||
ImVec2 m_ViewTransformPosition;
|
||||
|
||||
int m_SuspendCounter = 0;
|
||||
|
||||
float m_LastFringeScale = 1.0f;
|
||||
|
||||
ImVec2 m_MousePosBackup;
|
||||
ImVec2 m_MousePosPrevBackup;
|
||||
ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
|
||||
ImVec2 m_WindowCursorMaxBackup;
|
||||
|
||||
# if defined(IMGUI_HAS_VIEWPORT)
|
||||
ImVec2 m_WindowPosBackup;
|
||||
ImVec2 m_ViewportPosBackup;
|
||||
ImVec2 m_ViewportSizeBackup;
|
||||
# if IMGUI_VERSION_NUM > 18002
|
||||
ImVec2 m_ViewportWorkPosBackup;
|
||||
ImVec2 m_ViewportWorkSizeBackup;
|
||||
# else
|
||||
ImVec2 m_ViewportWorkOffsetMinBackup;
|
||||
ImVec2 m_ViewportWorkOffsetMaxBackup;
|
||||
# endif
|
||||
# endif
|
||||
};
|
||||
|
||||
} // namespace ImGuiEx
|
||||
|
||||
# endif // __IMGUI_EX_CANVAS_H__
|
||||
@@ -0,0 +1,76 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// LICENSE
|
||||
// This software is dual-licensed to the public domain and under the following
|
||||
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||
// publish, and distribute this file as you see fit.
|
||||
//
|
||||
// CREDITS
|
||||
// Written by Michal Cichon
|
||||
//------------------------------------------------------------------------------
|
||||
# ifndef __IMGUI_EXTRA_MATH_H__
|
||||
# define __IMGUI_EXTRA_MATH_H__
|
||||
# pragma once
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include <imgui.h>
|
||||
# ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
# define IMGUI_DEFINE_MATH_OPERATORS
|
||||
# endif
|
||||
# include <imgui_internal.h>
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
struct ImLine
|
||||
{
|
||||
ImVec2 A, B;
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Modern ImGui (1.89.4+) already defines these four operators in imgui.h
|
||||
// itself when IMGUI_DEFINE_MATH_OPERATORS is set before its first include —
|
||||
// guard against redefinition against this project's ImGui version.
|
||||
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
|
||||
inline bool operator==(const ImVec2& lhs, const ImVec2& rhs);
|
||||
inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs);
|
||||
inline ImVec2 operator*(const float lhs, const ImVec2& rhs);
|
||||
inline ImVec2 operator-(const ImVec2& lhs);
|
||||
# endif
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline float ImLength(float v);
|
||||
inline float ImLength(const ImVec2& v);
|
||||
inline float ImLengthSqr(float v);
|
||||
inline ImVec2 ImNormalized(const ImVec2& v);
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline bool ImRect_IsEmpty(const ImRect& rect);
|
||||
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge);
|
||||
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius);
|
||||
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b);
|
||||
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b);
|
||||
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b);
|
||||
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
namespace ImEasing {
|
||||
|
||||
template <typename V, typename T>
|
||||
inline V EaseOutQuad(V b, V c, T t)
|
||||
{
|
||||
return b - c * (t * (t - 2));
|
||||
}
|
||||
|
||||
} // namespace ImEasing
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# include "imgui_extra_math.inl"
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
# endif // __IMGUI_EXTRA_MATH_H__
|
||||