Compare commits

...

12 Commits

Author SHA1 Message Date
indigo d9826b7bbb Add in-canvas node thumbnails to material editor graph
Texture nodes show their decoded source image (background thread);
material nodes show a shader-ball render, amortized one-per-frame.
Also classifies shader nodes by Hypershade-style category and adds
an Arnold preset graph alongside the existing USD/MaterialX ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 15:08:56 +08:00
indigo 7d214821f5 Drag-to-create-and-connect nodes; double-click to rename materials
Drag a link off a pin onto empty canvas to open a create-node menu at
the drop point (the editor's QueryNewNode path), filtered to nodes with a
type-connectable pin; the chosen node is created and auto-wired to the
source pin. Direction is handled both ways — drag from an output makes a
downstream consumer, from an input an upstream producer. Type matching is
lenient (exact, or same scalar/2-/3-/4-vector class), mirroring what USD/
MaterialX networks already tolerate; the target pin is the first exact
match else the first connectable. RenderNodeSearchMenu gained an optional
linkSource arg so the TAB popup and this share one implementation, and
CreateShaderNode now returns the created path. Create and connect are two
undo steps.

Materials list: double-click a row starts an inline rename (mirrors
SceneHierarchyPanel — Enter commits, focus-loss/Esc cancels) through
RenamePrimCommand, which fixes up bindings/arcs via UsdNamespaceEditor.
Selection and the open graph follow the rename. This repurposes the row's
double-click, which previously opened the graph; opening stays on the
Show Graph button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:13:36 +08:00
indigo 2ddeeed061 Icon toolbar and resizable browser lists in material editor; mute debug logs
Toolbar: every action is now an icon button (tooltip carries the label,
text fallback with no icon set) via a local iconBtn helper matching the
StageEditor/Timeline convention — Create Material, Presets, Create + Bind,
Bind Current. Icons fill the standard button height (GetFrameHeight, zero
frame padding) so the row stays uniform.

Browser: the materials list and the create-node list are now split by a
draggable HorizontalSplitter (sibling to the column VerticalSplitter)
instead of a fixed 45% / separator; m_browserListRatio holds the list's
share (clamped 0.15-0.85). Create-node entries get a per-category glyph
(Material/Texture/Geometry/Light/Utility) sized to the text line height.

Silence the temporary node-editor drag-regression tracing: the
[NodeGraph] event-trace block is #if 0'd, its PersistNodePosition logs
commented, and the [ed] g_AxNodeEditorDebugLog hook install is commented
out (the vendored editor's dumps are guarded on that hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:16:04 +08:00
indigo 038450a1ca Vendor node editor in-tree; add material presets; debug drag regression
Move the imgui-node-editor subset the build actually compiles from
third_party/imgui-node-editor to src/ui/NodeEditor (version-controlled,
MIT LICENSE included). FindImguiNodeEditor.cmake points at the new
location; CMakeLists excludes src/ui/NodeEditor from UI_SOURCES so it
isn't compiled twice.

Material editor presets: a Presets menu (disabled with no open material)
builds a UsdPreviewSurface + UsdUVTexture graph fed by an st primvar
reader, or a MaterialX standard_surface + image graph fed by a texcoord
node. Each is one idempotent, undoable command that re-normalizes layout.
Create Material becomes an icon button. New nodes route through
FindFreeCanvasSpot so a creation never lands on top of an existing node
(overlapping nodes fight over the editor hit test and become undraggable).

Shader-ball preview: pick a previewable output (terminal or 3/4-component
color-like) instead of always the first output, so scalar-only nodes keep
the whole-material preview rather than failing Storm codegen. Per-shape
camera frame-fit margins and auto-clip framing.

Fixes: DeletePrimCommand::Undo recreates missing destination ancestors
before SdfCopySpec (parent material may have been deleted after the
command ran). ConfigWindowsMoveFromTitleBarOnly stops a content-area drag
in the node canvas from moving the whole Material Editor window.

Temporary (marked for removal once the node-editor drag regression is
diagnosed): main.cpp mirrors LOG_INFO to %APPDATA%\UsdLayerManager\
debug.log; a g_AxNodeEditorDebugLog hook in the vendored editor plus a
[NodeGraph] event-trace block in RenderNodeGraphCanvas dump click/drag/
selection/position-save state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:35:23 +08:00
indigo 0f30e080e3 Add preview shape picker to shader ball; drag controls for node properties
Preview geometry dropdown: Sphere / Cube / Cylinder (native prims),
Teapot and Hair (bundled resources/preview/*.usdc - teapot tessellated
from the Newell patch data with the Blinn 1.3x height correction, hair
as 220 procedural B-spline strands), and Cloud Volume (UsdVolVolume
over the OpenVDB bunny_cloud sample, downloaded at CMake configure and
gitignored like the HDRIs). Shapes live under one scope with the
material bound on it; switching activates one shape and reframes the
camera. Missing assets drop out of the dropdown. usdVol added to the
USD link set. The camera starts in a Maya-style 3/4 view.

Node property values switch from Input* fields back to draggable
controls (Ctrl+click to type): live-apply while dragging, one undo
command on release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:39:16 +08:00
indigo 6b8cb2406e Move preview renderer and HDR pickers above the shader ball
Drop the 'Shader Ball' label; the renderer switch and HDR environment
dropdowns sit side by side at the top of the preview column, above the
swatch. Both pickers take an optional item width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:26:39 +08:00
indigo c5ca6a7501 Show composition arcs and asset info in PropertyPanel
Prims with authored references or payloads get a section below the
prim header listing every list-op entry (explicit/add/prepend/append/
delete) with its asset path and target prim; a folder icon button
browses (USD/usdz/MaterialX/Alembic filter) and replaces the entry's
asset path, authored as one undoable whole-list-op swap into the edit
target. Prims with composed assetInfo get a read-only key/value section
(resolved paths shown in tooltips). PropertyPanel now receives the
IconManager for the browse button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:16:58 +08:00
indigo de94decf64 Persist material editor column widths in preferences
MaterialBrowserWidth / MaterialPreviewWidth in preferences.ini, saved
from the live splitter positions on shutdown and restored (clamped to
the splitter ranges) at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:54:06 +08:00
indigo 42fd355c53 Group create-node list by source and category; make editor columns resizable
The create-node list is now a two-level tree: a header per node source
(USD, MaterialX, Arnold, Cycles - from the Sdr sourceType, with glslfx
and OSL merged into USD so core nodes don't appear twice) containing
Hypershade-style category sub-trees (Material / Texture / Geometry /
Utility / Light) derived from Sdr context, terminal output types, role,
and name keywords. Duplicate collapsing is per (source, label) so
same-named nodes from different providers both stay; searching forces
all sections open. The TAB popup shows [Source/Category] per entry.

The browser and preview columns get drag splitters (viewport-divider
styling, clamped widths); the canvas absorbs the remainder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 06:47:12 +08:00
indigo b95b567b75 Require Shift to dock windows
io.ConfigDockingWithShift: plain title-bar/tab drags just move a
window; holding Shift shows the dock-position overlay and docks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
indigo bb9a5d9428 Author shader-ball node-preview overrides in the session layer
Fixes 'No spec at </Materials/...> when trying to set field
primChildren' during pin edits: the per-revision wipe used raw spec
surgery (RemoveNameChild/RemoveProperty) plus a ClearReferences/
AddReference cycle that briefly leaves the material's over spec empty
and inert, which Sdf cleanup can delete out from under later authoring.

The reference to the source material is now authored only when the
material changes (shared in-memory layers recompose live), and all
node-preview overrides live in the preview stage's session layer,
wiped wholesale with SdfLayer::Clear — no spec-level removal remains,
and switching materials no longer leaves stale overrides behind. The
edit target is pinned to the root layer so the rig never lands in the
cleared session layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
indigo b08bb6b233 Style material editor node properties like PropertyPanel
Same table anatomy as the attribute inspector: authored-state (a)
mini-button with path/type tooltip and copy-path popup, fixed name
column, stretch value column with row striping. Widgets switch to the
same vocabulary (InputFloat/InputScalarN %.4f, InputInt, default
ColorEdit swatches) committing on deactivation, and connected inputs
render as '-> node.output'. Preview column widens to 320 to fit; the
shader ball centers in it. Per-edit undo commands are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
42 changed files with 15454 additions and 134 deletions
+1
View File
@@ -79,3 +79,4 @@ imgui.ini
# OCIO config LUT files — downloaded automatically at CMake configure time # OCIO config LUT files — downloaded automatically at CMake configure time
resources/OpenColorIO-Configs/ resources/OpenColorIO-Configs/
resources/hdri/ resources/hdri/
resources/vdb/
+39 -1
View File
@@ -173,6 +173,11 @@ endif()
# Collect source files # Collect source files
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp") file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
file(GLOB_RECURSE UI_SOURCES "src/ui/*.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") file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp")
set(MAIN_SOURCE "src/main.cpp") set(MAIN_SOURCE "src/main.cpp")
@@ -606,6 +611,33 @@ foreach(_hdri ${_hdri_files})
endif() endif()
endforeach() 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 # Copy resources to build directory
file(COPY ${CMAKE_SOURCE_DIR}/resources file(COPY ${CMAKE_SOURCE_DIR}/resources
DESTINATION ${CMAKE_BINARY_DIR} DESTINATION ${CMAKE_BINARY_DIR}
@@ -624,7 +656,13 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/hdri" "${CMAKE_SOURCE_DIR}/resources/hdri"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri" "$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri"
COMMENT "Copying fonts, SVG icons and HDRI presets to build output" 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"
COMMENT "Copying fonts, SVG icons, HDRI presets and preview shapes to build output"
) )
# Copy ACES OCIO config to build output — only on first build (skipped if already present). # Copy ACES OCIO config to build output — only on first build (skipped if already present).
+11 -7
View File
@@ -1,9 +1,13 @@
# FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor # FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor
# #
# This module looks for imgui-node-editor in the third_party/imgui-node-editor # This module looks for imgui-node-editor at src/ui/NodeEditor (an in-tree,
# directory (vendored, docking branch) and compiles its sources directly # version-controlled subset of the upstream library — MIT licensed, see
# against this project's own Imgui::Imgui target so it shares the exact same # src/ui/NodeEditor/LICENSE — containing only the files this project's build
# ImGui headers/defines (no second copy of ImGui is pulled in). # 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: # Inputs:
# IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root # IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root
@@ -13,12 +17,12 @@
# ImguiNodeEditor::ImguiNodeEditor (imported STATIC target) # ImguiNodeEditor::ImguiNodeEditor (imported STATIC target)
if(NOT IMGUI_NODE_EDITOR_DIR) if(NOT IMGUI_NODE_EDITOR_DIR)
set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/NodeEditor") set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor")
endif() endif()
if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR) if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR)
set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR
"${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/Examples/Common/BlueprintUtilities") "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor/BlueprintWidgets")
endif() endif()
find_path(ImguiNodeEditor_INCLUDE_DIR find_path(ImguiNodeEditor_INCLUDE_DIR
@@ -48,7 +52,7 @@ endforeach()
include(FindPackageHandleStandardArgs) include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ImguiNodeEditor find_package_handle_standard_args(ImguiNodeEditor
REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR
FAIL_MESSAGE "imgui-node-editor not found — expected it vendored at third_party/imgui-node-editor/NodeEditor" FAIL_MESSAGE "imgui-node-editor not found — expected it at src/ui/NodeEditor"
) )
if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING) if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING)
+1
View File
@@ -59,6 +59,7 @@ set(OpenUSD_REQUIRED_LIBS
usdGeom usdGeom
usdLux usdLux
usdShade usdShade
usdVol
usdImaging usdImaging
usdImagingGL usdImagingGL
hdx hdx
Binary file not shown.
Binary file not shown.
+85 -1
View File
@@ -1,5 +1,7 @@
#include "MaterialManager.h" #include "MaterialManager.h"
#include <pxr/usd/sdr/registry.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/usd/primRange.h>
#include <pxr/usd/usdShade/material.h> #include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h> #include <pxr/usd/usdShade/shader.h>
@@ -9,10 +11,84 @@
#include <pxr/base/tf/token.h> #include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h> #include <pxr/base/vt/value.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <set> #include <set>
namespace UsdLayerManager { 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() { const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
if (m_shaderNodeCacheBuilt) if (m_shaderNodeCacheBuilt)
return m_shaderNodeCache; return m_shaderNodeCache;
@@ -24,12 +100,19 @@ const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes(
info.identifier = node->GetIdentifier().GetString(); info.identifier = node->GetIdentifier().GetString();
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName(); info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
info.family = node->GetFamily().GetString(); info.family = node->GetFamily().GetString();
info.category = DeriveCategory(node);
info.source = DeriveSource(node);
m_shaderNodeCache.push_back(std::move(info)); m_shaderNodeCache.push_back(std::move(info));
} }
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(), std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) { [](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
if (a.family != b.family) return a.family < b.family; 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; return a.label < b.label;
}); });
@@ -102,6 +185,7 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
pxr::SdrShaderNodeConstPtr sdrNode = pxr::SdrShaderNodeConstPtr sdrNode =
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(shaderId); pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(shaderId);
if (sdrNode) { if (sdrNode) {
node.category = DeriveCategory(sdrNode);
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) { for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
if (auto* prop = sdrNode->GetShaderInput(inputName)) if (auto* prop = sdrNode->GetShaderInput(inputName))
node.inputs.push_back({inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()}); node.inputs.push_back({inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
+14 -1
View File
@@ -15,7 +15,15 @@ namespace UsdLayerManager {
struct ShaderNodeTypeInfo { struct ShaderNodeTypeInfo {
std::string identifier; ///< Sdr identifier, authored as info:id std::string identifier; ///< Sdr identifier, authored as info:id
std::string label; ///< display label for the create-node menu std::string label; ///< display label for the create-node menu
std::string family; ///< grouping for the create-node menu (may be empty) 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 /// One input or output pin on a shader node, with its USD value type so
@@ -31,6 +39,11 @@ struct ShaderPinInfo {
struct ShaderGraphNode { struct ShaderGraphNode {
pxr::SdfPath path; pxr::SdfPath path;
std::string shaderId; 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}; pxr::GfVec2f uiPosition{0.0f, 0.0f};
/// False when no uiPosition custom data is authored (typical for networks /// False when no uiPosition custom data is authored (typical for networks
/// referenced from .mtlx) — the editor auto-lays such nodes out instead. /// referenced from .mtlx) — the editor auto-lays such nodes out instead.
+8
View File
@@ -59,6 +59,14 @@ void DeletePrimCommand::Undo() {
if (!rootLayer) return; if (!rootLayer) return;
try { 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)) if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath))
LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString()); LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString());
} catch (const std::exception& e) { } catch (const std::exception& e) {
+11 -1
View File
@@ -5,6 +5,7 @@
#include <exception> #include <exception>
#include <Windows.h> #include <Windows.h>
#include <string> #include <string>
#include <filesystem>
static void SetUsdPluginPath() { static void SetUsdPluginPath() {
char exePath[MAX_PATH]; char exePath[MAX_PATH];
@@ -98,7 +99,16 @@ static void SetUsdPluginPath() {
int main(int /*argc*/, char* /*argv*/[]) { int main(int /*argc*/, char* /*argv*/[]) {
try { try {
UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info); 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 ==="); LOG_INFO("=== USD Layer Manager Starting ===");
SetUsdPluginPath(); SetUsdPluginPath();
+17
View File
@@ -111,6 +111,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_iconManager = std::make_unique<IconManager>(); m_iconManager = std::make_unique<IconManager>();
m_iconManager->Initialize(ResourcePath("resources/icons"), 24); m_iconManager->Initialize(ResourcePath("resources/icons"), 24);
m_sceneHierarchyPanel->SetIconManager(m_iconManager.get()); m_sceneHierarchyPanel->SetIconManager(m_iconManager.get());
m_propertyPanel->SetIconManager(m_iconManager.get());
m_viewportPanel->SetIconManager(m_iconManager.get()); m_viewportPanel->SetIconManager(m_iconManager.get());
m_timelinePanel->SetIconManager(m_iconManager.get()); m_timelinePanel->SetIconManager(m_iconManager.get());
m_stageEditorPanel->SetIconManager(m_iconManager.get()); m_stageEditorPanel->SetIconManager(m_iconManager.get());
@@ -424,11 +425,25 @@ void Application::LoadPreferences()
else if (key == "OcioView") m_prefs.ocioView = val; else if (key == "OcioView") m_prefs.ocioView = val;
else if (key == "OcioColorSpace") m_prefs.ocioColorSpace = val; else if (key == "OcioColorSpace") m_prefs.ocioColorSpace = val;
else if (key == "OcioLook") m_prefs.ocioLook = 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; } }();
} }
if (m_materialEditorPanel)
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
m_prefs.materialPreviewWidth);
} }
void Application::SavePreferences() 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); std::ofstream f(m_prefsPath);
if (!f) return; if (!f) return;
f << "[Preferences]\n"; f << "[Preferences]\n";
@@ -438,6 +453,8 @@ void Application::SavePreferences()
f << "OcioView=" << m_prefs.ocioView << "\n"; f << "OcioView=" << m_prefs.ocioView << "\n";
f << "OcioColorSpace=" << m_prefs.ocioColorSpace << "\n"; f << "OcioColorSpace=" << m_prefs.ocioColorSpace << "\n";
f << "OcioLook=" << m_prefs.ocioLook << "\n"; f << "OcioLook=" << m_prefs.ocioLook << "\n";
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
} }
void Application::ApplyPrefsToAllViewports() void Application::ApplyPrefsToAllViewports()
+2
View File
@@ -28,6 +28,8 @@ struct AppPreferences {
std::string ocioView; std::string ocioView;
std::string ocioColorSpace; std::string ocioColorSpace;
std::string ocioLook; std::string ocioLook;
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
}; };
class Application { class Application {
+14
View File
@@ -94,6 +94,20 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei
// With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any // With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any
// ImGui window is focused, which would block all viewport hotkeys (F, A, etc.). // ImGui window is focused, which would block all viewport hotkeys (F, A, etc.).
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; 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. // Store imgui.ini in %APPDATA%\UsdLayerManager\ so it doesn't litter the CWD.
if (const char* appData = getenv("APPDATA")) { if (const char* appData = getenv("APPDATA")) {
File diff suppressed because it is too large Load Diff
+67 -4
View File
@@ -4,6 +4,7 @@
#include "../core/MaterialManager.h" #include "../core/MaterialManager.h"
#include "IconManager.h" #include "IconManager.h"
#include "MaterialPreviewRenderer.h" #include "MaterialPreviewRenderer.h"
#include "NodeThumbnailCache.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h> #include <pxr/usd/sdf/valueTypeName.h>
@@ -12,6 +13,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -44,6 +46,14 @@ public:
/// auto-loads it into the canvas. /// auto-loads it into the canvas.
void SetTargetPrimPath(const std::string& path); 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);
void Render(); void Render();
private: private:
@@ -51,7 +61,7 @@ private:
struct PinInfo { struct PinInfo {
pxr::SdfPath nodePath; pxr::SdfPath nodePath;
std::string name; std::string name;
bool isOutput; bool isOutput = false;
pxr::SdfValueTypeName typeName; pxr::SdfValueTypeName typeName;
}; };
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId. /// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
@@ -74,13 +84,28 @@ private:
void RenderMaterialBrowser(); void RenderMaterialBrowser();
/// Creates a uniquely-named empty material under /Materials and opens it. /// Creates a uniquely-named empty material under /Materials and opens it.
void CreateNewMaterial(); 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 RenderNodeGraphCanvas();
void RenderPreviewPanel(); void RenderPreviewPanel();
void HandleCreateAndDelete(); void HandleCreateAndDelete();
/// Nuke-style TAB popup: type-to-filter shader node list; Enter or click /// Nuke-style TAB popup: type-to-filter shader node list; Enter or click
/// creates the highlighted node at canvasPos. /// creates the highlighted node at canvasPos. When linkSource is non-null
void RenderNodeSearchMenu(const ImVec2& canvasPos); /// (a connection was dragged onto empty canvas), the list is filtered to
void CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos); /// 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 /// Binds an existing material to targetPath, undoably, restoring whatever
/// direct binding (if any) targetPath had before. /// direct binding (if any) targetPath had before.
void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath); void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath);
@@ -90,6 +115,9 @@ private:
void CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput); void CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput);
void DeleteNode(const pxr::SdfPath& nodePath); void DeleteNode(const pxr::SdfPath& nodePath);
void DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput); 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 SyncFromUsd();
void PersistNodePosition(ax::NodeEditor::NodeId nodeId); void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const; bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
@@ -123,6 +151,8 @@ private:
std::unordered_map<uintptr_t, LinkInfo> m_linkIdToInfo; std::unordered_map<uintptr_t, LinkInfo> m_linkIdToInfo;
MaterialPreviewRenderer m_preview; MaterialPreviewRenderer m_preview;
/// Per-node in-canvas thumbnails (texture images + material shader balls).
NodeThumbnailCache m_thumbnails;
ImVec2 m_pendingCreateNodePos{0.0f, 0.0f}; ImVec2 m_pendingCreateNodePos{0.0f, 0.0f};
@@ -133,6 +163,30 @@ private:
char m_nodeSearchBuf[128] = ""; char m_nodeSearchBuf[128] = "";
int m_nodeSearchSelected = 0; 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;
/// 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. /// Search text of the browser's persistent create-node list.
char m_browserNodeSearchBuf[128] = ""; char m_browserNodeSearchBuf[128] = "";
/// Shader id clicked in the browser's create-node list; created at the /// Shader id clicked in the browser's create-node list; created at the
@@ -140,6 +194,15 @@ private:
/// is only valid inside the editor's Begin/End). /// is only valid inside the editor's Begin/End).
std::string m_pendingCreateShaderId; 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); /// First node currently selected in the graph editor (empty = none);
/// drives the properties section under the preview. /// drives the properties section under the preview.
pxr::SdfPath m_selectedNodePath; pxr::SdfPath m_selectedNodePath;
+323 -35
View File
@@ -1,9 +1,20 @@
#include "MaterialPreviewRenderer.h" #include "MaterialPreviewRenderer.h"
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/PathUtils.h" #include "../utils/PathUtils.h"
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usdGeom/sphere.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/metrics.h>
#include <pxr/usd/usdGeom/tokens.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/materialBindingAPI.h>
#include <pxr/usd/usdShade/material.h> #include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h> #include <pxr/usd/usdShade/shader.h>
@@ -14,6 +25,7 @@
#include <pxr/usd/usdLux/tokens.h> #include <pxr/usd/usdLux/tokens.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h> #include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usd/references.h> #include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/sdf/assetPath.h> #include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/primSpec.h> #include <pxr/usd/sdf/primSpec.h>
#include <imgui.h> #include <imgui.h>
@@ -22,10 +34,30 @@
namespace UsdLayerManager { namespace UsdLayerManager {
namespace { namespace {
const pxr::SdfPath kSpherePath("/Preview/Sphere"); const pxr::SdfPath kShapesRootPath("/Preview/Shapes");
const pxr::SdfPath kDistantLightPath("/Preview/Light"); const pxr::SdfPath kDistantLightPath("/Preview/Light");
const pxr::SdfPath kDomeLightPath("/Preview/DomeLight"); 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 /// Scratch UsdPreviewSurface authored inside the (referenced) material scope
/// when previewing a non-terminal node output — child of the material so the /// when previewing a non-terminal node output — child of the material so the
/// connection stays encapsulated. /// connection stays encapsulated.
@@ -42,17 +74,227 @@ const LightPreset kLightPresets[] = {
{"Sunset", "venice_sunset_1k.exr"}, {"Sunset", "venice_sunset_1k.exr"},
}; };
constexpr int kLightPresetCount = static_cast<int>(sizeof(kLightPresets) / sizeof(kLightPresets[0])); 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 } // namespace
void MaterialPreviewRenderer::EnsureInitialized() { void MaterialPreviewRenderer::EnsureInitialized() {
if (m_initialized) return; if (m_initialized) return;
m_previewStage = pxr::UsdStage::CreateInMemory(); 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 // Pin the up axis rather than inheriting the site fallback: the light
// rig, dome-pole compensation, and camera framing all assume Y-up. // rig, dome-pole compensation, and camera framing all assume Y-up.
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y); pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
pxr::UsdGeomSphere sphere = pxr::UsdGeomSphere::Define(m_previewStage, kSpherePath);
sphere.GetRadiusAttr().Set(1.0); // 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 // Storm renders fine with zero authored lights (it injects a GL headlight
// as a fallback), but proper Hydra delegates like Embree/Arnold shade // as a fallback), but proper Hydra delegates like Embree/Arnold shade
@@ -78,10 +320,14 @@ void MaterialPreviewRenderer::EnsureInitialized() {
m_renderer.SetComplexity(1.3f); m_renderer.SetComplexity(1.3f);
ApplyLightPreset(m_lightPreset); ApplyLightPreset(m_lightPreset);
ApplyPreviewShape(m_previewShape); // activates one shape + frames the camera
m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds()); // Maya-style default 3/4 view: orbit the framed camera ~45° around and
m_camera.FrameSelection(m_previewBBox, 1.6); // extra margin so the sphere doesn't touch the frame edge // ~25° down instead of the dead-on front view (subsequent shape switches
m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); // 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_initialized = true;
m_dirty = true; m_dirty = true;
@@ -112,39 +358,37 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
// materials can be spread across sublayers. Referencing at the identical // materials can be spread across sublayers. Referencing at the identical
// path keeps internal connection paths valid without remapping, and // path keeps internal connection paths valid without remapping, and
// relative asset paths keep resolving against their original layers. // relative asset paths keep resolving against their original layers.
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath); // Authored only when the material changes: the shared in-memory layers
if (!mirrorPrim) { // recompose live, so revision bumps need no re-referencing.
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString()); if (materialPath != m_materialPath) {
return; 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);
} }
mirrorPrim.GetReferences().ClearReferences();
mirrorPrim.GetReferences().AddReference(
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath); pxr::UsdPrim shapesRoot = m_previewStage->GetPrimAtPath(kShapesRootPath);
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath)); pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
if (!material) { if (!material) {
LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString()); LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString());
return; return;
} }
if (spherePrim) if (shapesRoot) // bound on the scope: inherits to whichever shape is active
pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material); pxr::UsdShadeMaterialBindingAPI::Apply(shapesRoot).Bind(material);
// ── Selected-node preview (Hypershade-style) ───────────────────────── // ── Selected-node preview (Hypershade-style) ─────────────────────────
// Wipe any override left by a previous selection: the scratch wrapper // All node-preview overrides (the scratch wrapper shader and the local
// shader and the local opinions on the material's surface outputs, all // opinions on the material's surface outputs) live exclusively in the
// of which live only in the scratch stage's root layer. // preview stage's session layer, which is stronger than the root layer's
pxr::SdfLayerHandle rootLayer = m_previewStage->GetRootLayer(); // reference. Wiping them is then just clearing that layer — no per-spec
if (pxr::SdfPrimSpecHandle matSpec = rootLayer->GetPrimAtPath(materialPath)) { // surgery (RemoveNameChild/RemoveProperty) that could interact badly with
if (pxr::SdfPrimSpecHandle wrapSpec = // Sdf's inert-spec cleanup, and no stale overrides on other materials.
rootLayer->GetPrimAtPath(materialPath.AppendChild(kNodePreviewShaderName))) pxr::SdfLayerHandle sessionLayer = m_previewStage->GetSessionLayer();
matSpec->RemoveNameChild(wrapSpec); sessionLayer->Clear();
for (const char* outName : {"outputs:surface", "outputs:mtlx:surface"}) {
if (pxr::SdfPropertySpecHandle prop =
rootLayer->GetPropertyAtPath(materialPath.AppendProperty(pxr::TfToken(outName))))
matSpec->RemoveProperty(prop);
}
}
// Route the material's surface through the selected node. Nodes outside // Route the material's surface through the selected node. Nodes outside
// the material's subtree (possible in hand-authored cross-scope networks) // the material's subtree (possible in hand-authored cross-scope networks)
@@ -154,6 +398,8 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
previewNodePath.HasPrefix(materialPath)) { previewNodePath.HasPrefix(materialPath)) {
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath)); pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
if (nodeShader) { if (nodeShader) {
pxr::UsdEditContext sessionEdit(m_previewStage,
pxr::UsdEditTarget(sessionLayer));
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI(); pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
pxr::TfToken sourceName(previewNodeOutput); pxr::TfToken sourceName(previewNodeOutput);
if (!previewOutputIsTerminal) { if (!previewOutputIsTerminal) {
@@ -215,6 +461,47 @@ void MaterialPreviewRenderer::ApplyLightPreset(int index) {
m_dirty = 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() { void MaterialPreviewRenderer::UpdateDomeOrientation() {
if (!m_previewStage) return; if (!m_previewStage) return;
pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath); pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath);
@@ -252,9 +539,9 @@ void MaterialPreviewRenderer::UpdateDomeOrientation() {
m_dirty = true; m_dirty = true;
} }
void MaterialPreviewRenderer::RenderLightingDropdown() { void MaterialPreviewRenderer::RenderLightingDropdown(float width) {
if (!m_initialized) return; if (!m_initialized) return;
ImGui::SetNextItemWidth(-1.0f); ImGui::SetNextItemWidth(width);
if (ImGui::BeginCombo("##PreviewLighting", kLightPresets[m_lightPreset].label)) { if (ImGui::BeginCombo("##PreviewLighting", kLightPresets[m_lightPreset].label)) {
for (int i = 0; i < kLightPresetCount; ++i) { for (int i = 0; i < kLightPresetCount; ++i) {
if (ImGui::Selectable(kLightPresets[i].label, i == m_lightPreset) && i != m_lightPreset) if (ImGui::Selectable(kLightPresets[i].label, i == m_lightPreset) && i != m_lightPreset)
@@ -288,7 +575,8 @@ uint32_t MaterialPreviewRenderer::Render(int width, int height) {
void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) { void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) {
if (!m_initialized) return; if (!m_initialized) return;
m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity
m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); m_renderer.SetCameraStateFromGfCamera(
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
m_dirty = true; m_dirty = true;
} }
@@ -304,13 +592,13 @@ void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string&
m_dirty = true; m_dirty = true;
} }
void MaterialPreviewRenderer::RenderRendererDropdown() { void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
pxr::TfToken currentId = m_renderer.GetCurrentRendererId(); pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
std::string displayName = currentId.IsEmpty() std::string displayName = currentId.IsEmpty()
? "Renderer" ? "Renderer"
: UsdSceneRenderer::GetRendererDisplayName(currentId); : UsdSceneRenderer::GetRendererDisplayName(currentId);
ImGui::Button(displayName.c_str(), ImVec2(-1.0f, 0.0f)); ImGui::Button(displayName.c_str(), ImVec2(width, 0.0f));
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Shader-ball render delegate"); ImGui::SetTooltip("Shader-ball render delegate");
+13 -2
View File
@@ -50,16 +50,26 @@ public:
const std::string& ocioLook); const std::string& ocioLook);
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern). /// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
void RenderRendererDropdown(); /// width: ImGui item width (-1 = fill available).
void RenderRendererDropdown(float width = -1.0f);
/// HDR-environment picker (External / Room / Interior / Sunset). Presets /// HDR-environment picker (External / Room / Interior / Sunset). Presets
/// map to bundled resources/hdri/*.exr dome-light textures; a preset /// map to bundled resources/hdri/*.exr dome-light textures; a preset
/// whose file is missing falls back to the distant key light. /// whose file is missing falls back to the distant key light.
void RenderLightingDropdown(); /// 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: private:
void EnsureInitialized(); void EnsureInitialized();
void ApplyLightPreset(int index); 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 /// Compensates the dome-light pole convention per render delegate: Storm
/// samples with the pole along local +Y, spec-following delegates /// samples with the pole along local +Y, spec-following delegates
/// (hdArnold, hdCycles) along local +Z. Called on preset and renderer /// (hdArnold, hdCycles) along local +Z. Called on preset and renderer
@@ -76,6 +86,7 @@ private:
pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material) pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material)
std::string m_previewNodeOutput; std::string m_previewNodeOutput;
int m_lightPreset = 0; int m_lightPreset = 0;
int m_previewShape = 0;
bool m_initialized = false; bool m_initialized = false;
bool m_dirty = true; bool m_dirty = true;
int m_lastWidth = 0; int m_lastWidth = 0;
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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__
+21
View File
@@ -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.
+814
View File
@@ -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
+223
View File
@@ -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__
+452
View File
@@ -0,0 +1,452 @@
# 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();
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;
}
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;
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();
}
+250
View File
@@ -0,0 +1,250 @@
// 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();
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;
};
} // 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__
@@ -0,0 +1,191 @@
//------------------------------------------------------------------------------
// 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_INL__
# define __IMGUI_EXTRA_MATH_INL__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_extra_math.h"
//------------------------------------------------------------------------------
// See guard note in imgui_extra_math.h — already provided by imgui.h on
// this project's ImGui version.
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
inline bool operator==(const ImVec2& lhs, const ImVec2& rhs)
{
return lhs.x == rhs.x && lhs.y == rhs.y;
}
inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs)
{
return lhs.x != rhs.x || lhs.y != rhs.y;
}
inline ImVec2 operator*(const float lhs, const ImVec2& rhs)
{
return ImVec2(lhs * rhs.x, lhs * rhs.y);
}
inline ImVec2 operator-(const ImVec2& lhs)
{
return ImVec2(-lhs.x, -lhs.y);
}
# endif
//------------------------------------------------------------------------------
inline float ImLength(float v)
{
return v;
}
inline float ImLength(const ImVec2& v)
{
return ImSqrt(ImLengthSqr(v));
}
inline float ImLengthSqr(float v)
{
return v * v;
}
inline ImVec2 ImNormalized(const ImVec2& v)
{
return v * ImInvLength(v, 0.0f);
}
//------------------------------------------------------------------------------
inline bool ImRect_IsEmpty(const ImRect& rect)
{
return rect.Min.x >= rect.Max.x
|| rect.Min.y >= rect.Max.y;
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge)
{
if (!snap_to_edge && rect.Contains(p))
return p;
return ImVec2(
(p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x),
(p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y)
);
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius)
{
auto point = ImRect_ClosestPoint(rect, p, snap_to_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 = ImSqrt(distance_sq);
return point + offset * (ImMin(distance, radius) * (1.0f / distance));
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other)
{
ImVec2 result;
if (other.Min.x >= rect.Max.x)
result.x = rect.Max.x;
else if (other.Max.x <= rect.Min.x)
result.x = rect.Min.x;
else
result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2;
if (other.Min.y >= rect.Max.y)
result.y = rect.Max.y;
else if (other.Max.y <= rect.Min.y)
result.y = rect.Min.y;
else
result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2;
return result;
}
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b)
{
ImLine result;
result.A = ImRect_ClosestPoint(rect_a, rect_b);
result.B = ImRect_ClosestPoint(rect_b, rect_a);
auto distribute = [](float& a, float& b, float a0, float a1, float b0, float 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(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x);
distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y);
return result;
}
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b)
{
auto line = ImRect_ClosestLine(rect_a, rect_b);
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 = ImSqrt(length_sq);
const auto direction = ImVec2(offset.x / length, offset.y / length);
const auto total_radius_sq = radius_a_sq + radius_b_sq;
if (total_radius_sq > length_sq)
{
const auto scale = length / (radius_a + radius_b);
radius_a *= scale;
radius_b *= scale;
}
line.A = line.A + (direction * radius_a);
line.B = line.B - (direction * radius_b);
return line;
}
//------------------------------------------------------------------------------
# endif // __IMGUI_EXTRA_MATH_INL__
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,637 @@
//------------------------------------------------------------------------------
// 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 "imgui_node_editor_internal.h"
# include <algorithm>
//------------------------------------------------------------------------------
static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr;
//------------------------------------------------------------------------------
template <typename C, typename I, typename F>
static int BuildIdList(C& container, I* list, int listSize, F&& accept)
{
if (list != nullptr)
{
int count = 0;
for (auto object : container)
{
if (listSize <= 0)
break;
if (accept(object))
{
list[count] = I(object->ID().AsPointer());
++count;
--listSize;
}
}
return count;
}
else
return static_cast<int>(std::count_if(container.begin(), container.end(), accept));
}
//------------------------------------------------------------------------------
ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor(const Config* config)
{
return reinterpret_cast<ax::NodeEditor::EditorContext*>(new ax::NodeEditor::Detail::EditorContext(config));
}
void ax::NodeEditor::DestroyEditor(EditorContext* ctx)
{
if (GetCurrentEditor() == ctx)
SetCurrentEditor(nullptr);
auto editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
delete editor;
}
void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx)
{
s_Editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
}
ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor()
{
return reinterpret_cast<ax::NodeEditor::EditorContext*>(s_Editor);
}
ax::NodeEditor::Style& ax::NodeEditor::GetStyle()
{
return s_Editor->GetStyle();
}
const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex)
{
return s_Editor->GetStyle().GetColorName(colorIndex);
}
void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, const ImVec4& color)
{
s_Editor->GetStyle().PushColor(colorIndex, color);
}
void ax::NodeEditor::PopStyleColor(int count)
{
s_Editor->GetStyle().PopColor(count);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PopStyleVar(int count)
{
s_Editor->GetStyle().PopVar(count);
}
void ax::NodeEditor::Begin(const char* id, const ImVec2& size)
{
s_Editor->Begin(id, size);
}
void ax::NodeEditor::End()
{
s_Editor->End();
}
void ax::NodeEditor::BeginNode(NodeId id)
{
s_Editor->GetNodeBuilder().Begin(id);
}
void ax::NodeEditor::BeginPin(PinId id, PinKind kind)
{
s_Editor->GetNodeBuilder().BeginPin(id, kind);
}
void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b)
{
s_Editor->GetNodeBuilder().PinRect(a, b);
}
void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b)
{
s_Editor->GetNodeBuilder().PinPivotRect(a, b);
}
void ax::NodeEditor::PinPivotSize(const ImVec2& size)
{
s_Editor->GetNodeBuilder().PinPivotSize(size);
}
void ax::NodeEditor::PinPivotScale(const ImVec2& scale)
{
s_Editor->GetNodeBuilder().PinPivotScale(scale);
}
void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment)
{
s_Editor->GetNodeBuilder().PinPivotAlignment(alignment);
}
void ax::NodeEditor::EndPin()
{
s_Editor->GetNodeBuilder().EndPin();
}
void ax::NodeEditor::Group(const ImVec2& size)
{
s_Editor->GetNodeBuilder().Group(size);
}
void ax::NodeEditor::EndNode()
{
s_Editor->GetNodeBuilder().End();
}
bool ax::NodeEditor::BeginGroupHint(NodeId nodeId)
{
return s_Editor->GetHintBuilder().Begin(nodeId);
}
ImVec2 ax::NodeEditor::GetGroupMin()
{
return s_Editor->GetHintBuilder().GetGroupMin();
}
ImVec2 ax::NodeEditor::GetGroupMax()
{
return s_Editor->GetHintBuilder().GetGroupMax();
}
ImDrawList* ax::NodeEditor::GetHintForegroundDrawList()
{
return s_Editor->GetHintBuilder().GetForegroundDrawList();
}
ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList()
{
return s_Editor->GetHintBuilder().GetBackgroundDrawList();
}
void ax::NodeEditor::EndGroupHint()
{
s_Editor->GetHintBuilder().End();
}
ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node);
else
return nullptr;
}
bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, float thickness/* = 1.0f*/)
{
return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness);
}
void ax::NodeEditor::Flow(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
s_Editor->Flow(link);
}
bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness)
{
auto& context = s_Editor->GetItemCreator();
if (context.Begin())
{
context.SetStyle(ImColor(color), thickness);
return true;
}
else
return false;
}
bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.QueryLink(startId, endId) == Result::True;
}
bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.QueryLink(startId, endId);
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
bool ax::NodeEditor::QueryNewNode(PinId* pinId)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.QueryNode(pinId) == Result::True;
}
bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.QueryNode(pinId);
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
bool ax::NodeEditor::AcceptNewItem()
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.AcceptItem() == Result::True;
}
bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.AcceptItem();
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
void ax::NodeEditor::RejectNewItem()
{
auto& context = s_Editor->GetItemCreator();
context.RejectItem();
}
void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
if (context.RejectItem() != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
}
void ax::NodeEditor::EndCreate()
{
auto& context = s_Editor->GetItemCreator();
context.End();
}
bool ax::NodeEditor::BeginDelete()
{
auto& context = s_Editor->GetItemDeleter();
return context.Begin();
}
bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, PinId* endId)
{
auto& context = s_Editor->GetItemDeleter();
return context.QueryLink(linkId, startId, endId);
}
bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId)
{
auto& context = s_Editor->GetItemDeleter();
return context.QueryNode(nodeId);
}
bool ax::NodeEditor::AcceptDeletedItem()
{
auto& context = s_Editor->GetItemDeleter();
return context.AcceptItem();
}
void ax::NodeEditor::RejectDeletedItem()
{
auto& context = s_Editor->GetItemDeleter();
context.RejectItem();
}
void ax::NodeEditor::EndDelete()
{
auto& context = s_Editor->GetItemDeleter();
context.End();
}
void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position)
{
s_Editor->SetNodePosition(nodeId, position);
}
ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId)
{
return s_Editor->GetNodePosition(nodeId);
}
ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId)
{
return s_Editor->GetNodeSize(nodeId);
}
void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
node->CenterOnScreenInNextFrame();
}
void ax::NodeEditor::RestoreNodeState(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
s_Editor->MarkNodeToRestoreState(node);
}
void ax::NodeEditor::Suspend()
{
s_Editor->Suspend();
}
void ax::NodeEditor::Resume()
{
s_Editor->Resume();
}
bool ax::NodeEditor::IsSuspended()
{
return s_Editor->IsSuspended();
}
bool ax::NodeEditor::IsActive()
{
return s_Editor->IsActive();
}
bool ax::NodeEditor::HasSelectionChanged()
{
return s_Editor->HasSelectionChanged();
}
int ax::NodeEditor::GetSelectedObjectCount()
{
return (int)s_Editor->GetSelectedObjects().size();
}
int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
{
return object->AsNode() != nullptr;
});
}
int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
{
return object->AsLink() != nullptr;
});
}
void ax::NodeEditor::ClearSelection()
{
s_Editor->ClearSelection();
}
void ax::NodeEditor::SelectNode(NodeId nodeId, bool append)
{
if (auto node = s_Editor->FindNode(nodeId))
{
if (append)
s_Editor->SelectObject(node);
else
s_Editor->SetSelectedObject(node);
}
}
void ax::NodeEditor::SelectLink(LinkId linkId, bool append)
{
if (auto link = s_Editor->FindLink(linkId))
{
if (append)
s_Editor->SelectObject(link);
else
s_Editor->SetSelectedObject(link);
}
}
void ax::NodeEditor::DeselectNode(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
s_Editor->DeselectObject(node);
}
void ax::NodeEditor::DeselectLink(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
s_Editor->DeselectObject(link);
}
bool ax::NodeEditor::DeleteNode(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
return s_Editor->GetItemDeleter().Add(node);
else
return false;
}
bool ax::NodeEditor::DeleteLink(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
return s_Editor->GetItemDeleter().Add(link);
else
return false;
}
void ax::NodeEditor::NavigateToContent(float duration)
{
s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration);
}
void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration)
{
s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration);
}
bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId)
{
return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId);
}
bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId)
{
return s_Editor->GetContextMenu().ShowPinContextMenu(pinId);
}
bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId)
{
return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId);
}
bool ax::NodeEditor::ShowBackgroundContextMenu()
{
return s_Editor->GetContextMenu().ShowBackgroundContextMenu();
}
void ax::NodeEditor::EnableShortcuts(bool enable)
{
s_Editor->EnableShortcuts(enable);
}
bool ax::NodeEditor::AreShortcutsEnabled()
{
return s_Editor->AreShortcutsEnabled();
}
bool ax::NodeEditor::BeginShortcut()
{
return s_Editor->GetShortcut().Begin();
}
bool ax::NodeEditor::AcceptCut()
{
return s_Editor->GetShortcut().AcceptCut();
}
bool ax::NodeEditor::AcceptCopy()
{
return s_Editor->GetShortcut().AcceptCopy();
}
bool ax::NodeEditor::AcceptPaste()
{
return s_Editor->GetShortcut().AcceptPaste();
}
bool ax::NodeEditor::AcceptDuplicate()
{
return s_Editor->GetShortcut().AcceptDuplicate();
}
bool ax::NodeEditor::AcceptCreateNode()
{
return s_Editor->GetShortcut().AcceptCreateNode();
}
int ax::NodeEditor::GetActionContextSize()
{
return static_cast<int>(s_Editor->GetShortcut().m_Context.size());
}
int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
{
return object->AsNode() != nullptr;
});
}
int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
{
return object->AsLink() != nullptr;
});
}
void ax::NodeEditor::EndShortcut()
{
return s_Editor->GetShortcut().End();
}
float ax::NodeEditor::GetCurrentZoom()
{
return s_Editor->GetView().InvScale;
}
ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode()
{
return s_Editor->GetDoubleClickedNode();
}
ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin()
{
return s_Editor->GetDoubleClickedPin();
}
ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink()
{
return s_Editor->GetDoubleClickedLink();
}
bool ax::NodeEditor::IsBackgroundClicked()
{
return s_Editor->IsBackgroundClicked();
}
bool ax::NodeEditor::IsBackgroundDoubleClicked()
{
return s_Editor->IsBackgroundDoubleClicked();
}
bool ax::NodeEditor::PinHadAnyLinks(PinId pinId)
{
return s_Editor->PinHadAnyLinks(pinId);
}
ImVec2 ax::NodeEditor::GetScreenSize()
{
return s_Editor->GetRect().GetSize();
}
ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos)
{
return s_Editor->ToCanvas(pos);
}
ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos)
{
return s_Editor->ToScreen(pos);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// 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_INTERNAL_INL__
# define __IMGUI_NODE_EDITOR_INTERNAL_INL__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_node_editor_internal.h"
//------------------------------------------------------------------------------
namespace ax {
namespace NodeEditor {
namespace Detail {
//------------------------------------------------------------------------------
//inline ImRect ToRect(const ax::rectf& rect)
//{
// return ImRect(
// to_imvec(rect.top_left()),
// to_imvec(rect.bottom_right())
// );
//}
//
//inline ImRect ToRect(const ax::rect& rect)
//{
// return ImRect(
// to_imvec(rect.top_left()),
// to_imvec(rect.bottom_right())
// );
//}
inline ImRect ImGui_GetItemRect()
{
return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
}
//------------------------------------------------------------------------------
} // namespace Detail
} // namespace Editor
} // namespace ax
//------------------------------------------------------------------------------
# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__
+352
View File
@@ -0,0 +1,352 @@
#include "NodeThumbnailCache.h"
#include "../utils/GLExt.h"
#include "../utils/Logger.h"
#include <pxr/imaging/hio/image.h>
#include <pxr/imaging/hio/types.h>
#include <pxr/base/gf/half.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace UsdLayerManager {
namespace {
// Mix a graph revision with a node path so switching which node the shared
// preview renderer draws always reads as "changed" to MaterialPreviewRenderer's
// revision gate (it treats the value as an opaque change token), while a real
// graph edit (revision bump) still forces a re-render.
size_t RenderToken(size_t revision, const std::string& nodeKey) {
size_t h = revision * 1099511628211ull;
h ^= std::hash<std::string>{}(nodeKey) + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2);
return h;
}
} // namespace
NodeThumbnailCache::NodeThumbnailCache() {
// Prime Hio's plugin registry on the main thread so the worker never
// first-touches plugin discovery off-thread.
pxr::HioImage::IsSupportedImageFile("prime.png");
m_running = true;
m_worker = std::thread(&NodeThumbnailCache::WorkerLoop, this);
}
NodeThumbnailCache::~NodeThumbnailCache() {
m_running = false;
m_jobCv.notify_all();
if (m_worker.joinable())
m_worker.join();
Clear();
if (m_readFbo) glDeleteFramebuffers(1, &m_readFbo);
if (m_drawFbo) glDeleteFramebuffers(1, &m_drawFbo);
}
// ---------------------------------------------------------------------------
// Texture thumbnails
// ---------------------------------------------------------------------------
ImTextureID NodeThumbnailCache::GetTextureThumbnail(const pxr::SdfPath& node,
const std::string& resolvedFile) {
const std::string key = node.GetString();
TexEntry& e = m_texEntries[key];
e.touched = true;
if (resolvedFile.empty())
return e.texId;
// (Re)request when the file differs from both the uploaded and in-flight one.
if (resolvedFile != e.fileKey && resolvedFile != e.reqFile) {
e.reqFile = resolvedFile;
{
std::lock_guard<std::mutex> lk(m_jobMutex);
m_jobs.push_back(TexJob{key, resolvedFile});
}
m_jobCv.notify_one();
}
return e.texId;
}
void NodeThumbnailCache::WorkerLoop() {
for (;;) {
TexJob job;
{
std::unique_lock<std::mutex> lk(m_jobMutex);
m_jobCv.wait(lk, [this] { return !m_running || !m_jobs.empty(); });
if (!m_running) return;
job = std::move(m_jobs.front());
m_jobs.pop_front();
}
TexResult result;
result.nodeKey = job.nodeKey;
result.file = job.file;
result.ok = DecodeThumbnail(job.file, result);
{
std::lock_guard<std::mutex> lk(m_resultMutex);
m_results.push_back(std::move(result));
}
}
}
bool NodeThumbnailCache::DecodeThumbnail(const std::string& file, TexResult& out) {
pxr::HioImageSharedPtr img = pxr::HioImage::OpenForReading(file, /*subimage=*/0, /*mip=*/0,
pxr::HioImage::SourceColorSpace::Auto,
/*suppressErrors=*/true);
if (!img) return false;
const int sw = img->GetWidth();
const int sh = img->GetHeight();
if (sw <= 0 || sh <= 0) return false;
// Hio's stb reader does no format conversion: it requires the requested
// StorageSpec.format to *exactly* equal the file's own format (channel
// count + component type + sRGB-ness) or it raises "Image format mismatch".
// So read into the image's native format and normalize to RGBA8 ourselves.
const pxr::HioFormat fmt = img->GetFormat();
const int nComp = pxr::HioGetComponentCount(fmt);
const pxr::HioType type = pxr::HioGetHioType(fmt);
if (nComp < 1 || nComp > 4) return false;
size_t bpc = 0; // bytes per component
switch (type) {
case pxr::HioTypeUnsignedByte:
case pxr::HioTypeUnsignedByteSRGB:
case pxr::HioTypeSignedByte: bpc = 1; break;
case pxr::HioTypeUnsignedShort:
case pxr::HioTypeSignedShort:
case pxr::HioTypeHalfFloat: bpc = 2; break;
case pxr::HioTypeUnsignedInt:
case pxr::HioTypeInt:
case pxr::HioTypeFloat: bpc = 4; break;
case pxr::HioTypeDouble: bpc = 8; break;
default: return false;
}
const size_t pixels = static_cast<size_t>(sw) * sh;
std::vector<unsigned char> raw(pixels * nComp * bpc);
pxr::HioImage::StorageSpec spec;
spec.width = sw;
spec.height = sh;
spec.depth = 1;
spec.format = fmt;
spec.flipped = false; // keep top row first for ImGui display
spec.data = raw.data();
if (!img->Read(spec)) return false;
// Collapse whatever component type the file uses down to an 8-bit value.
// sRGB byte data is passed through as-is (ImGui does no color management,
// so the encoded bytes display correctly); float/half is clamped to [0,1].
const void* rp = raw.data();
auto toU8 = [&](size_t i) -> unsigned char {
switch (type) {
case pxr::HioTypeUnsignedByte:
case pxr::HioTypeUnsignedByteSRGB:
return reinterpret_cast<const uint8_t*>(rp)[i];
case pxr::HioTypeSignedByte:
return static_cast<unsigned char>(std::max(0, static_cast<int>(reinterpret_cast<const int8_t*>(rp)[i]) * 2));
case pxr::HioTypeUnsignedShort:
return static_cast<unsigned char>(reinterpret_cast<const uint16_t*>(rp)[i] >> 8);
case pxr::HioTypeSignedShort:
return static_cast<unsigned char>(std::clamp(static_cast<int>(reinterpret_cast<const int16_t*>(rp)[i]) >> 7, 0, 255));
case pxr::HioTypeHalfFloat:
return static_cast<unsigned char>(std::clamp(static_cast<float>(reinterpret_cast<const pxr::GfHalf*>(rp)[i]), 0.f, 1.f) * 255.f + 0.5f);
case pxr::HioTypeFloat:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const float*>(rp)[i], 0.f, 1.f) * 255.f + 0.5f);
case pxr::HioTypeUnsignedInt:
return static_cast<unsigned char>(reinterpret_cast<const uint32_t*>(rp)[i] >> 24);
case pxr::HioTypeInt:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const int32_t*>(rp)[i] >> 23, 0, 255));
case pxr::HioTypeDouble:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const double*>(rp)[i], 0.0, 1.0) * 255.0 + 0.5);
default: return 0;
}
};
// Expand to RGBA8: 1ch → grey, 2ch → grey+alpha, 3ch → opaque, 4ch → as-is.
std::vector<unsigned char> native(pixels * 4);
for (size_t px = 0; px < pixels; ++px) {
const size_t s = px * nComp;
const unsigned char c0 = toU8(s);
unsigned char* d = &native[px * 4];
d[0] = c0;
d[1] = nComp >= 3 ? toU8(s + 1) : c0;
d[2] = nComp >= 3 ? toU8(s + 2) : c0;
d[3] = nComp == 4 ? toU8(s + 3) : (nComp == 2 ? toU8(s + 1) : 255);
}
const int longSide = std::max(sw, sh);
const float scale = longSide > kThumbPx ? static_cast<float>(kThumbPx) / longSide : 1.0f;
const int dw = std::max(1, static_cast<int>(std::lround(sw * scale)));
const int dh = std::max(1, static_cast<int>(std::lround(sh * scale)));
if (dw == sw && dh == sh) {
out.rgba = std::move(native);
} else {
out.rgba.assign(static_cast<size_t>(dw) * dh * 4, 0);
for (int y = 0; y < dh; ++y) {
const int sy0 = y * sh / dh, sy1 = std::max(sy0 + 1, (y + 1) * sh / dh);
for (int x = 0; x < dw; ++x) {
const int sx0 = x * sw / dw, sx1 = std::max(sx0 + 1, (x + 1) * sw / dw);
unsigned int acc[4] = {0, 0, 0, 0};
unsigned int n = 0;
for (int sy = sy0; sy < sy1; ++sy)
for (int sx = sx0; sx < sx1; ++sx) {
const unsigned char* p = &native[(static_cast<size_t>(sy) * sw + sx) * 4];
acc[0] += p[0]; acc[1] += p[1]; acc[2] += p[2]; acc[3] += p[3];
++n;
}
unsigned char* d = &out.rgba[(static_cast<size_t>(y) * dw + x) * 4];
d[0] = static_cast<unsigned char>(acc[0] / n);
d[1] = static_cast<unsigned char>(acc[1] / n);
d[2] = static_cast<unsigned char>(acc[2] / n);
d[3] = static_cast<unsigned char>(acc[3] / n);
}
}
}
out.w = dw;
out.h = dh;
return true;
}
void NodeThumbnailCache::UploadTexture(TexEntry& e, const TexResult& r) {
DeleteTex(e.texId);
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, r.w, r.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, r.rgba.data());
glBindTexture(GL_TEXTURE_2D, 0);
e.texId = static_cast<ImTextureID>(static_cast<ImU64>(tex));
}
// ---------------------------------------------------------------------------
// Material thumbnails
// ---------------------------------------------------------------------------
ImTextureID NodeThumbnailCache::GetMaterialThumbnail(const pxr::UsdStageRefPtr& stage,
const pxr::SdfPath& materialPath,
const pxr::SdfPath& node,
const std::string& output, bool terminal,
size_t revision) {
MatEntry& e = m_matEntries[node.GetString()];
e.touched = true;
e.stage = stage;
e.materialPath = materialPath;
e.nodePath = node;
e.output = output;
e.terminal = terminal;
if (revision != e.revision || e.texId == 0) {
e.revision = revision;
e.stale = true;
}
return e.texId;
}
void NodeThumbnailCache::EnsureBlitFbos() {
if (!m_readFbo) glGenFramebuffers(1, &m_readFbo);
if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo);
}
void NodeThumbnailCache::BlitToEntry(MatEntry& e, uint32_t srcTex) {
int sw = 0, sh = 0;
glBindTexture(GL_TEXTURE_2D, srcTex);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &sw);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &sh);
glBindTexture(GL_TEXTURE_2D, 0);
if (sw <= 0 || sh <= 0) return;
if (e.texId == 0) {
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kThumbPx, kThumbPx, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
e.texId = static_cast<ImTextureID>(static_cast<ImU64>(tex));
}
const GLuint dst = static_cast<GLuint>(static_cast<ImU64>(e.texId));
EnsureBlitFbos();
GLint prevRead = 0, prevDraw = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDraw);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_readFbo);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTex, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_drawFbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0);
// Flip Y (dst rows reversed): the draw target is a GL bottom-up texture, so
// this leaves the cache texture top-down for uniform ImGui (0,0)-(1,1) UVs.
glBlitFramebuffer(0, 0, sw, sh, 0, kThumbPx, kThumbPx, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevRead));
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(prevDraw));
}
// ---------------------------------------------------------------------------
// Per-frame pump / lifecycle
// ---------------------------------------------------------------------------
void NodeThumbnailCache::PumpMainThread() {
// 1. Upload finished texture decodes.
std::vector<TexResult> done;
{
std::lock_guard<std::mutex> lk(m_resultMutex);
done.swap(m_results);
}
for (const TexResult& r : done) {
auto it = m_texEntries.find(r.nodeKey);
if (it == m_texEntries.end()) continue;
TexEntry& e = it->second;
if (r.file != e.reqFile) continue; // superseded by a newer request
if (r.ok) {
UploadTexture(e, r);
e.fileKey = r.file;
}
e.reqFile.clear();
}
// 2. Render at most one stale material thumbnail (amortized).
for (auto& kv : m_matEntries) {
MatEntry& e = kv.second;
if (!e.stale || !e.touched || !e.stage) continue;
m_matRenderer.SetMaterial(e.stage, e.materialPath, RenderToken(e.revision, kv.first),
e.nodePath, e.output, e.terminal);
const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx);
if (src != 0) {
BlitToEntry(e, src);
e.stale = false;
}
break; // one per frame
}
// 3. Prune entries not requested since the previous pump (node deleted or
// no longer a thumbnailed category), then reset touch flags for this frame.
for (auto it = m_texEntries.begin(); it != m_texEntries.end();) {
if (!it->second.touched) { DeleteTex(it->second.texId); it = m_texEntries.erase(it); }
else { it->second.touched = false; ++it; }
}
for (auto it = m_matEntries.begin(); it != m_matEntries.end();) {
if (!it->second.touched) { DeleteTex(it->second.texId); it = m_matEntries.erase(it); }
else { it->second.touched = false; ++it; }
}
}
void NodeThumbnailCache::Clear() {
for (auto& kv : m_texEntries) DeleteTex(kv.second.texId);
for (auto& kv : m_matEntries) DeleteTex(kv.second.texId);
m_texEntries.clear();
m_matEntries.clear();
}
void NodeThumbnailCache::DeleteTex(ImTextureID& id) {
if (id) {
GLuint t = static_cast<GLuint>(static_cast<ImU64>(id));
glDeleteTextures(1, &t);
id = 0;
}
}
} // namespace UsdLayerManager
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include "MaterialPreviewRenderer.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <imgui.h>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
namespace UsdLayerManager {
/// Owns the in-canvas node thumbnails for the material editor.
///
/// Two kinds, both handed back as an ImTextureID (0 while not ready):
/// - Texture nodes: the source image, decoded on a background worker thread
/// (USD HioImage / OpenImageIO) and uploaded to GL on the main thread.
/// - Material nodes: a shader-ball render through a dedicated
/// MaterialPreviewRenderer. Hydra is GL-/main-thread-bound, so these are
/// rendered one-per-frame (amortized) and cached by graph revision.
///
/// Lifecycle is driven from the panel: Get*Thumbnail() per node during the
/// canvas draw, then PumpMainThread() once per frame (GL context current).
class NodeThumbnailCache {
public:
NodeThumbnailCache();
~NodeThumbnailCache();
NodeThumbnailCache(const NodeThumbnailCache&) = delete;
NodeThumbnailCache& operator=(const NodeThumbnailCache&) = delete;
/// Displayed edge length; also the material render size.
static constexpr int kThumbPx = 96;
/// Returns the node's texture thumbnail (0 = not ready). Enqueues a
/// background decode when resolvedFile is new or has changed. An empty
/// resolvedFile (unresolvable asset) yields whatever is cached (usually 0).
ImTextureID GetTextureThumbnail(const pxr::SdfPath& node, const std::string& resolvedFile);
/// Returns the node's material shader-ball thumbnail (0 = not ready yet).
/// Marks it stale (to be re-rendered on a later pump) when revision changes.
/// output/terminal select the node output to preview (a token-typed
/// terminal is wired as the surface; otherwise routed into diffuseColor).
ImTextureID GetMaterialThumbnail(const pxr::UsdStageRefPtr& stage,
const pxr::SdfPath& materialPath,
const pxr::SdfPath& node,
const std::string& output, bool terminal,
size_t revision);
/// Main thread, once per frame, GL context current: drains finished
/// decodes and uploads them, renders at most one stale material thumbnail,
/// and prunes entries not requested since the previous pump.
void PumpMainThread();
/// Frees every thumbnail (call on material/stage change). Main thread.
void Clear();
private:
// ---- texture thumbnails (background decode) ----
struct TexJob { std::string nodeKey; std::string file; };
struct TexResult { std::string nodeKey; std::string file;
std::vector<unsigned char> rgba; int w = 0, h = 0; bool ok = false; };
struct TexEntry { ImTextureID texId = 0; std::string fileKey; std::string reqFile;
bool touched = false; };
void WorkerLoop();
static bool DecodeThumbnail(const std::string& file, TexResult& out);
void UploadTexture(TexEntry& e, const TexResult& r);
std::thread m_worker;
std::atomic<bool> m_running{false};
std::mutex m_jobMutex;
std::condition_variable m_jobCv;
std::deque<TexJob> m_jobs;
std::mutex m_resultMutex;
std::vector<TexResult> m_results;
std::unordered_map<std::string, TexEntry> m_texEntries;
// ---- material thumbnails (main-thread, amortized) ----
struct MatEntry {
ImTextureID texId = 0;
size_t revision = 0;
bool stale = true;
bool touched = false;
pxr::UsdStageRefPtr stage;
pxr::SdfPath materialPath;
pxr::SdfPath nodePath;
std::string output;
bool terminal = false;
};
std::unordered_map<std::string, MatEntry> m_matEntries;
MaterialPreviewRenderer m_matRenderer;
unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit
unsigned int m_drawFbo = 0;
void EnsureBlitFbos();
void BlitToEntry(MatEntry& e, uint32_t srcTex);
static void DeleteTex(ImTextureID& id);
};
} // namespace UsdLayerManager
+199 -1
View File
@@ -1,8 +1,16 @@
#include "PropertyPanel.h" #include "PropertyPanel.h"
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../core/commands/TransformCommand.h" #include "../core/commands/TransformCommand.h"
#include "../core/commands/AttributeSetCommand.h" #include "../core/commands/AttributeSetCommand.h"
#include <pxr/usd/sdf/listOp.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/payload.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/attribute.h> #include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/relationship.h> #include <pxr/usd/usd/relationship.h>
@@ -1116,6 +1124,186 @@ void PropertyPanel::RenderPrimHeader(const pxr::UsdPrim& prim) {
ImGui::EndTable(); ImGui::EndTable();
} }
// ---------------------------------------------------------------------------
// References & Payloads one row per list-op entry; clicking the asset path
// browses for a replacement file (undoable whole-list-op swap).
// ---------------------------------------------------------------------------
namespace {
pxr::SdfReference WithAssetPath(const pxr::SdfReference& r, const std::string& assetPath) {
return pxr::SdfReference(assetPath, r.GetPrimPath(), r.GetLayerOffset(), r.GetCustomData());
}
pxr::SdfPayload WithAssetPath(const pxr::SdfPayload& p, const std::string& assetPath) {
return pxr::SdfPayload(assetPath, p.GetPrimPath(), p.GetLayerOffset());
}
// Shared row renderer for SdfReferenceListOp / SdfPayloadListOp: one row per
// item in each list-op bucket, labelled with its op. On replace, the whole
// modified list op is authored to the edit target as one undoable command.
template <typename ListOpT>
void RenderArcListRows(const pxr::UsdPrim& prim, CommandHistory* history,
IconManager* icons,
const char* tag, const char* displayKind,
const pxr::TfToken& fieldKey, const ListOpT& listOp, int& uid)
{
using ItemVector = typename ListOpT::ItemVector;
struct Bucket { const char* opName; ItemVector items; };
Bucket buckets[] = {
{"explicit", listOp.GetExplicitItems()},
{"add", listOp.GetAddedItems()},
{"prepend", listOp.GetPrependedItems()},
{"append", listOp.GetAppendedItems()},
{"delete", listOp.GetDeletedItems()},
};
const float rowH = ImGui::GetFrameHeight();
for (size_t b = 0; b < sizeof(buckets) / sizeof(buckets[0]); ++b) {
for (size_t i = 0; i < buckets[b].items.size(); ++i) {
const auto& item = buckets[b].items[i];
std::string display = item.GetAssetPath();
if (!item.GetPrimPath().IsEmpty())
display += " <" + item.GetPrimPath().GetString() + ">";
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
ImGui::PushID(uid++);
ImGui::TableSetColumnIndex(0);
DrawPropertyMiniButton(tag, /*authored=*/true,
item.GetAssetPath().c_str(), displayKind);
ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("%s", buckets[b].opName);
ImGui::TableSetColumnIndex(2);
// Browse icon button first, then the path as plain text.
bool clicked;
if (icons) {
clicked = ImGui::ImageButton("##browseArc",
ImTextureRef(icons->Get(Icon::FolderOpen)),
ImVec2(14.f, 14.f));
} else {
clicked = ImGui::SmallButton("...");
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Browse for a replacement file");
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(display.empty() ? "(no asset)" : display.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s (%s)\n%s", displayKind, buckets[b].opName,
item.GetAssetPath().c_str());
if (clicked) {
// Anything USD can compose is a valid arc target: native USD
// layers plus file formats with layer plugins (MaterialX,
// Alembic).
static const char* kArcFileFilter =
"Referenceable (*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc)\0"
"*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc\0"
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0"
"MaterialX (*.mtlx)\0*.mtlx\0"
"Alembic (*.abc)\0*.abc\0"
"All Files (*.*)\0*.*\0";
std::string picked = FileDialog::OpenFile(kArcFileFilter, "Select Reference File");
if (!picked.empty() && picked != item.GetAssetPath()) {
ListOpT newOp = listOp;
ItemVector items = buckets[b].items;
items[i] = WithAssetPath(item, picked);
switch (b) {
case 0: newOp.SetExplicitItems(items); break;
case 1: newOp.SetAddedItems(items); break;
case 2: newOp.SetPrependedItems(items); break;
case 3: newOp.SetAppendedItems(items); break;
case 4: newOp.SetDeletedItems(items); break;
}
// Authors the whole (possibly multi-layer-composed) list
// op into the edit target — the pragmatic single-layer
// behaviour; undo restores the previous composed op.
if (history) {
pxr::UsdPrim p = prim;
ListOpT oldOp = listOp;
history->Push(std::make_unique<AttributeSetCommand>(
std::string("Replace ") + displayKind + " Path",
[p, fieldKey, newOp]() mutable { p.SetMetadata(fieldKey, newOp); },
[p, fieldKey, oldOp]() mutable { p.SetMetadata(fieldKey, oldOp); }));
} else {
pxr::UsdPrim(prim).SetMetadata(fieldKey, newOp);
}
}
}
ImGui::PopID();
}
}
}
} // namespace
void PropertyPanel::RenderCompositionArcsSection(const pxr::UsdPrim& prim) {
if (!ImGui::CollapsingHeader("References & Payloads", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##compArcs", 3, kFlags)) return;
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f);
ImGui::TableSetupColumn("##op", ImGuiTableColumnFlags_WidthFixed, 60.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
int uid = 0;
if (prim.HasAuthoredReferences()) {
pxr::SdfReferenceListOp refOp;
if (prim.GetMetadata(pxr::SdfFieldKeys->References, &refOp))
RenderArcListRows(prim, m_commandHistory, m_iconManager, "(R)", "Reference",
pxr::SdfFieldKeys->References, refOp, uid);
}
if (prim.HasAuthoredPayloads()) {
pxr::SdfPayloadListOp payloadOp;
if (prim.GetMetadata(pxr::SdfFieldKeys->Payload, &payloadOp))
RenderArcListRows(prim, m_commandHistory, m_iconManager, "(P)", "Payload",
pxr::SdfFieldKeys->Payload, payloadOp, uid);
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Asset Info read-only view of the composed assetInfo dictionary
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAssetInfoSection(const pxr::UsdPrim& prim) {
if (!ImGui::CollapsingHeader("Asset Info", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##assetInfo", 2, kFlags)) return;
ImGui::TableSetupColumn("##key", ImGuiTableColumnFlags_WidthFixed, 110.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
const pxr::VtDictionary info = prim.GetAssetInfo();
std::vector<std::string> keys;
keys.reserve(info.size());
for (const auto& entry : info)
keys.push_back(entry.first);
std::sort(keys.begin(), keys.end());
for (const std::string& key : keys) {
const pxr::VtValue& value = info.find(key)->second;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("%s", key.c_str());
ImGui::TableSetColumnIndex(1);
if (value.IsHolding<pxr::SdfAssetPath>()) {
const auto& assetPath = value.UncheckedGet<pxr::SdfAssetPath>();
ImGui::TextUnformatted(assetPath.GetAssetPath().c_str());
if (ImGui::IsItemHovered() && !assetPath.GetResolvedPath().empty())
ImGui::SetTooltip("resolved: %s", assetPath.GetResolvedPath().c_str());
} else {
const std::string text = pxr::TfStringify(value);
ImGui::TextUnformatted(text.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", text.c_str());
}
}
ImGui::EndTable();
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Variant Sets no borders, no header (mirrors DrawVariantSetsCombos in usdtweak) // Variant Sets no borders, no header (mirrors DrawVariantSetsCombos in usdtweak)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1605,9 +1793,19 @@ void PropertyPanel::Render() {
ImGui::Separator(); ImGui::Separator();
// Scrollable body variant sets → transform → all properties // Scrollable body composition arcs → variant sets → transform → all properties
ImGui::BeginChild("##body"); ImGui::BeginChild("##body");
if (prim.HasAuthoredReferences() || prim.HasAuthoredPayloads()) {
RenderCompositionArcsSection(prim);
ImGui::Separator();
}
if (!prim.GetAssetInfo().empty()) {
RenderAssetInfoSection(prim);
ImGui::Separator();
}
if (prim.HasVariantSets()) { if (prim.HasVariantSets()) {
RenderVariantSetsSection(prim); RenderVariantSetsSection(prim);
ImGui::Separator(); ImGui::Separator();
+10
View File
@@ -3,6 +3,7 @@
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/commands/TransformCommand.h" #include "../core/commands/TransformCommand.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/timeCode.h> #include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
@@ -25,6 +26,7 @@ public:
void SetPropertyManager(PropertyManager* manager); void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
void SetStage(pxr::UsdStageRefPtr stage); void SetStage(pxr::UsdStageRefPtr stage);
void SetSelectedPrimPath(const std::string& path); void SetSelectedPrimPath(const std::string& path);
@@ -60,12 +62,20 @@ private:
void RenderTransformSection(); void RenderTransformSection();
void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema
void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info
/// References/payloads authored on the prim, one row per list-op entry
/// (explicit/add/prepend/append/delete) with its asset path; clicking a
/// path browses for a replacement file (undoable).
void RenderCompositionArcsSection(const pxr::UsdPrim& prim);
/// Read-only view of the prim's composed assetInfo dictionary
/// (identifier, name, version, ...).
void RenderAssetInfoSection(const pxr::UsdPrim& prim);
void RenderAttrRelSubTable(const std::vector<pxr::UsdAttribute>& attrs, void RenderAttrRelSubTable(const std::vector<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& rels, const std::vector<pxr::UsdRelationship>& rels,
const pxr::UsdEditTarget& editTarget); ///< inner 3-col table const pxr::UsdEditTarget& editTarget); ///< inner 3-col table
PropertyManager* m_propertyManager = nullptr; PropertyManager* m_propertyManager = nullptr;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr;
pxr::UsdStageRefPtr m_stage; pxr::UsdStageRefPtr m_stage;
std::string m_selectedPrimPath; std::string m_selectedPrimPath;