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>
This commit is contained in:
@@ -26,6 +26,11 @@
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
// UsdLayerManager local patch hook in the vendored node editor (see
|
||||
// imgui_node_editor.cpp): when set, BuildControl logs its click-time
|
||||
// hit-test state through it. Temporary debugging aid.
|
||||
extern void (*g_AxNodeEditorDebugLog)(const char*);
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
namespace NE = ax::NodeEditor;
|
||||
@@ -221,6 +226,10 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) {
|
||||
} // namespace
|
||||
|
||||
MaterialEditorPanel::MaterialEditorPanel() {
|
||||
// Route the vendored editor's click-time hit-test dump into our log so
|
||||
// it interleaves with the [NodeGraph] event trace.
|
||||
g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
|
||||
|
||||
NE::Config config;
|
||||
config.SettingsFile = nullptr; // layout persists as USD uiPosition custom data, not an on-disk .json
|
||||
config.UserPointer = this;
|
||||
@@ -303,12 +312,30 @@ void MaterialEditorPanel::RenderPreviewPanel() {
|
||||
bool outputIsTerminal = false;
|
||||
if (!m_selectedNodePath.IsEmpty()) {
|
||||
for (const auto& node : m_graph.nodes) {
|
||||
if (node.path == m_selectedNodePath && !node.outputs.empty()) {
|
||||
previewNode = node.path;
|
||||
previewOutput = node.outputs.front().name;
|
||||
outputIsTerminal = (node.outputs.front().typeName == pxr::SdfValueTypeNames->Token);
|
||||
break;
|
||||
if (node.path != m_selectedNodePath)
|
||||
continue;
|
||||
// Pick a previewable output: a terminal (surface) or a
|
||||
// 3/4-component value the wrapper can plug into diffuseColor.
|
||||
// Scalar outputs (e.g. UsdUVTexture's Sdr-first output "r")
|
||||
// make Storm's codegen swizzle .xyz off a float and fail to
|
||||
// compile — nodes with only scalar outputs keep the
|
||||
// whole-material preview instead.
|
||||
const auto& tn = pxr::SdfValueTypeNames;
|
||||
for (const auto& out : node.outputs) {
|
||||
const pxr::SdfValueTypeName& t = out.typeName;
|
||||
const bool terminal = (t == tn->Token);
|
||||
const bool colorLike =
|
||||
t == tn->Color3f || t == tn->Float3 || t == tn->Vector3f ||
|
||||
t == tn->Normal3f || t == tn->Point3f ||
|
||||
t == tn->Color4f || t == tn->Float4;
|
||||
if (terminal || colorLike) {
|
||||
previewNode = node.path;
|
||||
previewOutput = out.name;
|
||||
outputIsTerminal = terminal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_preview.SetMaterial(m_stage, m_materialPath, ComputeGraphRevision(m_stage, m_graph),
|
||||
@@ -585,9 +612,47 @@ void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const Sha
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::RenderToolbar() {
|
||||
if (ImGui::Button("Create Material"))
|
||||
// Create Material icon button
|
||||
bool createClicked;
|
||||
if (m_iconManager) {
|
||||
createClicked = ImGui::ImageButton("##createMaterial",
|
||||
ImTextureRef(m_iconManager->Get(Icon::FilePlus)),
|
||||
ImVec2(18.f, 18.f));
|
||||
} else {
|
||||
createClicked = ImGui::Button("Create Material");
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Create Material");
|
||||
if (createClicked)
|
||||
CreateNewMaterial();
|
||||
|
||||
// Presets menu: entries appear per node source when the required shader
|
||||
// definitions are registered, and need an open material to apply to.
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(m_materialPath.IsEmpty());
|
||||
if (ImGui::Button("Presets"))
|
||||
ImGui::OpenPopup("MaterialPresets");
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::BeginPopup("MaterialPresets")) {
|
||||
auto& sdr = pxr::SdrRegistry::GetInstance();
|
||||
if (ImGui::BeginMenu("USD")) {
|
||||
ImGui::BeginDisabled(!sdr.GetShaderNodeByIdentifier(pxr::TfToken("UsdPreviewSurface")));
|
||||
if (ImGui::MenuItem("Default Shader Graph"))
|
||||
CreateUsdPresetGraph();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("MaterialX")) {
|
||||
ImGui::BeginDisabled(
|
||||
!sdr.GetShaderNodeByIdentifier(pxr::TfToken("ND_standard_surface_surfaceshader")));
|
||||
if (ImGui::MenuItem("Standard Surface Graph"))
|
||||
CreateMaterialXPresetGraph();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (!m_targetPrimPath.IsEmpty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Selected: %s", m_targetPrimPath.GetText());
|
||||
@@ -731,6 +796,159 @@ void MaterialEditorPanel::CreateNewMaterial() {
|
||||
OpenOrCreateMaterial(materialsScope.AppendChild(pxr::TfToken(finalName)).GetString());
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Shared by the preset builders below.
|
||||
pxr::UsdShadeShader DefinePresetShader(const pxr::UsdStageRefPtr& stage,
|
||||
const pxr::SdfPath& materialPath,
|
||||
const char* name, const char* shaderId,
|
||||
const pxr::GfVec2f& uiPos) {
|
||||
pxr::UsdShadeShader shader =
|
||||
pxr::UsdShadeShader::Define(stage, materialPath.AppendChild(pxr::TfToken(name)));
|
||||
shader.CreateIdAttr(pxr::VtValue(pxr::TfToken(shaderId)));
|
||||
shader.GetPrim().SetCustomDataByKey(pxr::TfToken("uiPosition"), pxr::VtValue(uiPos));
|
||||
return shader;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MaterialEditorPanel::CreateUsdPresetGraph() {
|
||||
if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return;
|
||||
// Re-applying onto an existing preset graph is allowed: the build is
|
||||
// idempotent (same prims/connections re-authored, user-set values like
|
||||
// texture file paths survive) and re-normalizes the node layout.
|
||||
|
||||
pxr::UsdStageRefPtr stage = m_stage;
|
||||
pxr::SdfPath matPath = m_materialPath;
|
||||
|
||||
auto build = [stage, matPath]() {
|
||||
// Column/row spacing must exceed the rendered node sizes (full Sdr
|
||||
// pin sets make these ~400px tall): overlapping nodes steal each
|
||||
// other's clicks in the editor's hit test and become undraggable.
|
||||
pxr::UsdShadeShader surf = DefinePresetShader(
|
||||
stage, matPath, "UsdPreviewSurface", "UsdPreviewSurface", pxr::GfVec2f(0.f, 400.f));
|
||||
pxr::UsdShadeShader st = DefinePresetShader(
|
||||
stage, matPath, "stReader", "UsdPrimvarReader_float2", pxr::GfVec2f(-840.f, 600.f));
|
||||
st.CreateInput(pxr::TfToken("varname"), pxr::SdfValueTypeNames->Token)
|
||||
.Set(pxr::TfToken("st"));
|
||||
|
||||
struct TexPreset {
|
||||
const char* name;
|
||||
const char* texOutput; pxr::SdfValueTypeName outType;
|
||||
const char* destInput; pxr::SdfValueTypeName destType;
|
||||
bool rawColorSpace;
|
||||
};
|
||||
const TexPreset textures[] = {
|
||||
{"diffuseTexture", "rgb", pxr::SdfValueTypeNames->Float3,
|
||||
"diffuseColor", pxr::SdfValueTypeNames->Color3f, false},
|
||||
{"metallicTexture", "r", pxr::SdfValueTypeNames->Float,
|
||||
"metallic", pxr::SdfValueTypeNames->Float, true},
|
||||
{"roughnessTexture", "r", pxr::SdfValueTypeNames->Float,
|
||||
"roughness", pxr::SdfValueTypeNames->Float, true},
|
||||
{"normalTexture", "rgb", pxr::SdfValueTypeNames->Float3,
|
||||
"normal", pxr::SdfValueTypeNames->Normal3f, true},
|
||||
};
|
||||
float y = 0.f;
|
||||
for (const TexPreset& t : textures) {
|
||||
pxr::UsdShadeShader tex = DefinePresetShader(
|
||||
stage, matPath, t.name, "UsdUVTexture", pxr::GfVec2f(-420.f, y));
|
||||
y += 440.f;
|
||||
tex.CreateInput(pxr::TfToken("st"), pxr::SdfValueTypeNames->Float2)
|
||||
.ConnectToSource(st.ConnectableAPI(), pxr::TfToken("result"));
|
||||
if (t.rawColorSpace)
|
||||
tex.CreateInput(pxr::TfToken("sourceColorSpace"), pxr::SdfValueTypeNames->Token)
|
||||
.Set(pxr::TfToken("raw"));
|
||||
surf.CreateInput(pxr::TfToken(t.destInput), t.destType)
|
||||
.ConnectToSource(tex.ConnectableAPI(), pxr::TfToken(t.texOutput));
|
||||
}
|
||||
// Normal maps need the [0,1] texture range remapped to [-1,1].
|
||||
pxr::UsdShadeShader normalTex(
|
||||
stage->GetPrimAtPath(matPath.AppendChild(pxr::TfToken("normalTexture"))));
|
||||
normalTex.CreateInput(pxr::TfToken("scale"), pxr::SdfValueTypeNames->Float4)
|
||||
.Set(pxr::GfVec4f(2.f, 2.f, 2.f, 1.f));
|
||||
normalTex.CreateInput(pxr::TfToken("bias"), pxr::SdfValueTypeNames->Float4)
|
||||
.Set(pxr::GfVec4f(-1.f, -1.f, -1.f, 0.f));
|
||||
|
||||
pxr::UsdShadeMaterial material(stage->GetPrimAtPath(matPath));
|
||||
if (material)
|
||||
material.CreateSurfaceOutput().ConnectToSource(
|
||||
surf.ConnectableAPI(), pxr::TfToken("surface"));
|
||||
};
|
||||
auto remove = [stage, matPath]() {
|
||||
for (const char* name : {"UsdPreviewSurface", "stReader", "diffuseTexture",
|
||||
"metallicTexture", "roughnessTexture", "normalTexture"})
|
||||
stage->RemovePrim(matPath.AppendChild(pxr::TfToken(name)));
|
||||
if (pxr::UsdPrim mat = stage->GetPrimAtPath(matPath))
|
||||
mat.RemoveProperty(pxr::TfToken("outputs:surface"));
|
||||
};
|
||||
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
"Create USD Preset Graph", build, remove));
|
||||
// Force a position reseed: on a re-apply the nodes are already known to
|
||||
// the editor at their old canvas positions.
|
||||
m_nodeIdToPath.clear();
|
||||
SyncFromUsd();
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::CreateMaterialXPresetGraph() {
|
||||
if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return;
|
||||
// Re-applying is allowed; see CreateUsdPresetGraph.
|
||||
|
||||
pxr::UsdStageRefPtr stage = m_stage;
|
||||
pxr::SdfPath matPath = m_materialPath;
|
||||
|
||||
auto build = [stage, matPath]() {
|
||||
// See the USD preset: spacing must exceed rendered node sizes or
|
||||
// overlapping nodes steal each other's drag hit-tests.
|
||||
pxr::UsdShadeShader surf = DefinePresetShader(
|
||||
stage, matPath, "standard_surface", "ND_standard_surface_surfaceshader",
|
||||
pxr::GfVec2f(0.f, 300.f));
|
||||
pxr::UsdShadeShader texcoord = DefinePresetShader(
|
||||
stage, matPath, "texcoord", "ND_texcoord_vector2", pxr::GfVec2f(-840.f, 400.f));
|
||||
|
||||
struct ImagePreset {
|
||||
const char* name;
|
||||
const char* shaderId; pxr::SdfValueTypeName outType;
|
||||
const char* destInput; pxr::SdfValueTypeName destType;
|
||||
};
|
||||
const ImagePreset images[] = {
|
||||
{"base_color_image", "ND_image_color3", pxr::SdfValueTypeNames->Color3f,
|
||||
"base_color", pxr::SdfValueTypeNames->Color3f},
|
||||
{"specular_roughness_image", "ND_image_float", pxr::SdfValueTypeNames->Float,
|
||||
"specular_roughness", pxr::SdfValueTypeNames->Float},
|
||||
{"metalness_image", "ND_image_float", pxr::SdfValueTypeNames->Float,
|
||||
"metalness", pxr::SdfValueTypeNames->Float},
|
||||
};
|
||||
float y = 0.f;
|
||||
for (const ImagePreset& img : images) {
|
||||
pxr::UsdShadeShader tex = DefinePresetShader(
|
||||
stage, matPath, img.name, img.shaderId, pxr::GfVec2f(-420.f, y));
|
||||
y += 400.f;
|
||||
tex.CreateInput(pxr::TfToken("texcoord"), pxr::SdfValueTypeNames->Float2)
|
||||
.ConnectToSource(texcoord.ConnectableAPI(), pxr::TfToken("out"));
|
||||
surf.CreateInput(pxr::TfToken(img.destInput), img.destType)
|
||||
.ConnectToSource(tex.ConnectableAPI(), pxr::TfToken("out"));
|
||||
}
|
||||
|
||||
// MaterialX networks terminate on the mtlx render-context output
|
||||
// (matches usdMtlx-imported materials).
|
||||
pxr::UsdShadeMaterial material(stage->GetPrimAtPath(matPath));
|
||||
if (material)
|
||||
material.CreateSurfaceOutput(pxr::TfToken("mtlx")).ConnectToSource(
|
||||
surf.ConnectableAPI(), pxr::TfToken("out"));
|
||||
};
|
||||
auto remove = [stage, matPath]() {
|
||||
for (const char* name : {"standard_surface", "texcoord", "base_color_image",
|
||||
"specular_roughness_image", "metalness_image"})
|
||||
stage->RemovePrim(matPath.AppendChild(pxr::TfToken(name)));
|
||||
if (pxr::UsdPrim mat = stage->GetPrimAtPath(matPath))
|
||||
mat.RemoveProperty(pxr::TfToken("outputs:mtlx:surface"));
|
||||
};
|
||||
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
"Create MaterialX Preset Graph", build, remove));
|
||||
m_nodeIdToPath.clear(); // reseed positions (see CreateUsdPresetGraph)
|
||||
SyncFromUsd();
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::OpenOrCreateMaterial(const std::string& pathStr) {
|
||||
if (!m_stage) return;
|
||||
if (!pxr::SdfPath::IsValidPathString(pathStr, nullptr)) {
|
||||
@@ -883,6 +1101,8 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
||||
NE::Begin("MaterialEditorCanvas", ImVec2(0.0f, 0.0f));
|
||||
|
||||
if (!m_pendingCreateShaderId.empty()) {
|
||||
// Browser-list creations land at the view center; CreateShaderNode
|
||||
// nudges the spot clear of existing nodes.
|
||||
CreateShaderNode(m_pendingCreateShaderId, NE::ScreenToCanvas(viewCenterScreen));
|
||||
m_pendingCreateShaderId.clear();
|
||||
}
|
||||
@@ -1001,6 +1221,164 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
||||
|
||||
NE::End();
|
||||
|
||||
// --- Debug tracing of node interaction events. Edge-triggered so each
|
||||
// event logs once, not per frame. Event order in the editor is:
|
||||
// LMB press -> node becomes active (and is brought to front) -> drag
|
||||
// moves it -> release -> "click" -> selection updates -> position save.
|
||||
{
|
||||
// Logs the mouse position and every node's editor rect with a hit
|
||||
// verdict, and returns the hit paths. Topmost is unknown to us, but
|
||||
// the full table makes overlap fights directly visible in the log.
|
||||
// hitIds, when given, collects the editor ids of the hit nodes.
|
||||
auto nodeRectsAtMouse = [this](std::vector<uintptr_t>* hitIds) {
|
||||
std::string hits;
|
||||
const ImVec2 screen = ImGui::GetMousePos();
|
||||
const ImVec2 mouse = NE::ScreenToCanvas(screen);
|
||||
char buf[512];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"[NodeGraph] hit test: mouse screen (%.1f, %.1f) canvas (%.1f, %.1f), %zu node(s)",
|
||||
screen.x, screen.y, mouse.x, mouse.y, m_graph.nodes.size());
|
||||
LOG_INFO(std::string(buf));
|
||||
for (const auto& node : m_graph.nodes) {
|
||||
NE::NodeId nodeId(HashId(node.path.GetString()));
|
||||
const ImVec2 pos = NE::GetNodePosition(nodeId);
|
||||
const ImVec2 size = NE::GetNodeSize(nodeId);
|
||||
if (pos.x == FLT_MAX || size.x <= 0.0f) {
|
||||
LOG_INFO("[NodeGraph] " + node.path.GetString() +
|
||||
": no editor rect yet (not rendered)");
|
||||
continue;
|
||||
}
|
||||
const bool hit = mouse.x >= pos.x && mouse.x <= pos.x + size.x &&
|
||||
mouse.y >= pos.y && mouse.y <= pos.y + size.y;
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"[NodeGraph] %s: rect min (%.1f, %.1f) size (%.1f x %.1f)%s",
|
||||
node.path.GetText(), pos.x, pos.y, size.x, size.y,
|
||||
hit ? " <-- HIT" : "");
|
||||
LOG_INFO(std::string(buf));
|
||||
if (hit) {
|
||||
hits += (hits.empty() ? "" : ", ") + node.path.GetString();
|
||||
if (hitIds)
|
||||
hitIds->push_back(nodeId.Get());
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
};
|
||||
|
||||
// Editor NodeIds of the currently selected nodes.
|
||||
auto selectedNodeIds = []() {
|
||||
std::vector<uintptr_t> ids;
|
||||
const int count = NE::GetSelectedObjectCount();
|
||||
if (count > 0) {
|
||||
std::vector<NE::NodeId> sel(static_cast<size_t>(count));
|
||||
const int nodeCount = NE::GetSelectedNodes(sel.data(), count);
|
||||
ids.reserve(static_cast<size_t>(nodeCount));
|
||||
for (int i = 0; i < nodeCount; ++i)
|
||||
ids.push_back(sel[static_cast<size_t>(i)].Get());
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
auto nodeNames = [this](const std::vector<uintptr_t>& ids) {
|
||||
std::string names;
|
||||
for (uintptr_t idValue : ids) {
|
||||
auto it = m_nodeIdToPath.find(idValue);
|
||||
names += (names.empty() ? "" : ", ");
|
||||
names += (it != m_nodeIdToPath.end()) ? it->second.GetName() : "<unknown>";
|
||||
}
|
||||
return names;
|
||||
};
|
||||
auto containsAnyOf = [](const std::vector<uintptr_t>& haystack,
|
||||
const std::vector<uintptr_t>& needles) {
|
||||
for (uintptr_t needle : needles)
|
||||
if (std::find(haystack.begin(), haystack.end(), needle) != haystack.end())
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (canvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
m_debugDragNodeIds.clear();
|
||||
const std::string hits = nodeRectsAtMouse(&m_debugDragNodeIds);
|
||||
m_debugPressOnNode = !hits.empty();
|
||||
m_debugDragLogged = false;
|
||||
if (m_debugPressOnNode) {
|
||||
// Pressing an already-selected node keeps the selection (no
|
||||
// clear, no rect-select) so a drag moves the whole group —
|
||||
// the editor only updates selection on release.
|
||||
const std::vector<uintptr_t> selected = selectedNodeIds();
|
||||
std::string msg = "[NodeGraph] LMB press over node rect(s): " + hits;
|
||||
if (selected.size() > 1 && containsAnyOf(selected, m_debugDragNodeIds))
|
||||
msg += " [in current selection of " + std::to_string(selected.size()) +
|
||||
" -> drag moves all selected]";
|
||||
LOG_INFO(msg);
|
||||
} else {
|
||||
LOG_INFO("[NodeGraph] LMB press on empty canvas (rect select / clear selection on release)");
|
||||
}
|
||||
}
|
||||
if (m_debugPressOnNode && !m_debugDragLogged &&
|
||||
ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||
m_debugDragLogged = true;
|
||||
// Mirror DragAction::Accept: dragging a selected node moves the
|
||||
// whole selection, so trace every group member from here on.
|
||||
const std::vector<uintptr_t> selected = selectedNodeIds();
|
||||
if (selected.size() > 1 && containsAnyOf(selected, m_debugDragNodeIds)) {
|
||||
m_debugDragNodeIds = selected;
|
||||
LOG_INFO("[NodeGraph] drag start: group move of " +
|
||||
std::to_string(selected.size()) + " selected node(s): " +
|
||||
nodeNames(m_debugDragNodeIds));
|
||||
} else {
|
||||
LOG_INFO("[NodeGraph] drag start (ready to move), cursor over: " + nodeRectsAtMouse(nullptr));
|
||||
}
|
||||
}
|
||||
// Per-move trace while a drag is live, only on frames the mouse
|
||||
// actually moved: mouse position, accumulated drag delta, and the
|
||||
// live editor position of every tracked node — the whole selection
|
||||
// for a group drag, else the node(s) hit at press time (a node that
|
||||
// stops tracking the delta shows up immediately).
|
||||
if (m_debugDragLogged && ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
|
||||
(io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) {
|
||||
const ImVec2 screen = ImGui::GetMousePos();
|
||||
const ImVec2 canvas = NE::ScreenToCanvas(screen);
|
||||
const ImVec2 delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0f);
|
||||
char buf[512];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"[NodeGraph] drag move: mouse screen (%.1f, %.1f) canvas (%.1f, %.1f) dragDelta (%.1f, %.1f)",
|
||||
screen.x, screen.y, canvas.x, canvas.y, delta.x, delta.y);
|
||||
std::string msg = buf;
|
||||
for (uintptr_t idValue : m_debugDragNodeIds) {
|
||||
const ImVec2 nodePos = NE::GetNodePosition(NE::NodeId(idValue));
|
||||
auto it = m_nodeIdToPath.find(idValue);
|
||||
std::snprintf(buf, sizeof(buf), "; %s at (%.1f, %.1f)",
|
||||
it != m_nodeIdToPath.end() ? it->second.GetName().c_str() : "<unknown>",
|
||||
nodePos.x, nodePos.y);
|
||||
msg += buf;
|
||||
}
|
||||
LOG_INFO(msg);
|
||||
}
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
|
||||
(m_debugPressOnNode || m_debugDragLogged)) {
|
||||
LOG_INFO(std::string("[NodeGraph] LMB release") +
|
||||
(m_debugDragLogged ? " (ends drag; position save should follow)" : " (click, no drag)"));
|
||||
m_debugPressOnNode = false;
|
||||
m_debugDragLogged = false;
|
||||
m_debugDragNodeIds.clear();
|
||||
}
|
||||
|
||||
// Covers single-click selection and rubber-band rect multi-select
|
||||
// alike; fires on the frame the editor's selection list changes.
|
||||
if (NE::HasSelectionChanged())
|
||||
{
|
||||
const int count = NE::GetSelectedObjectCount();
|
||||
std::vector<NE::NodeId> selected(static_cast<size_t>(std::max(count, 1)));
|
||||
const int nodeCount = NE::GetSelectedNodes(selected.data(), count);
|
||||
std::string msg = "[NodeGraph] selection changed: " + std::to_string(nodeCount) + " node(s)";
|
||||
for (int i = 0; i < nodeCount; ++i) {
|
||||
auto it = m_nodeIdToPath.find(selected[static_cast<size_t>(i)].Get());
|
||||
msg += (i == 0 ? ": " : ", ");
|
||||
msg += (it != m_nodeIdToPath.end()) ? it->second.GetString() : "<unknown>";
|
||||
}
|
||||
LOG_INFO(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror the editor's node selection (first selected node) for the
|
||||
// properties section under the preview.
|
||||
m_selectedNodePath = pxr::SdfPath();
|
||||
@@ -1124,6 +1502,54 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 MaterialEditorPanel::FindFreeCanvasSpot(ImVec2 desired, const std::string& shaderId) const {
|
||||
// Estimated footprint of the not-yet-rendered node: same height model as
|
||||
// ApplyFallbackLayout, width covering typical icon+label pin rows.
|
||||
auto estimateSize = [](size_t pinCount) {
|
||||
return ImVec2(340.0f, static_cast<float>(pinCount) * 24.0f + 80.0f);
|
||||
};
|
||||
size_t newPinCount = 6;
|
||||
if (pxr::SdrShaderNodeConstPtr sdrNode =
|
||||
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(pxr::TfToken(shaderId)))
|
||||
newPinCount = sdrNode->GetShaderInputNames().size() + sdrNode->GetShaderOutputNames().size();
|
||||
const ImVec2 newSize = estimateSize(newPinCount);
|
||||
|
||||
struct Rect { ImVec2 pos, size; };
|
||||
std::vector<Rect> rects;
|
||||
rects.reserve(m_graph.nodes.size());
|
||||
for (const auto& node : m_graph.nodes) {
|
||||
Rect r{ImVec2(node.uiPosition[0], node.uiPosition[1]),
|
||||
estimateSize(node.inputs.size() + node.outputs.size())};
|
||||
// Nodes the editor has already rendered report their real rectangle.
|
||||
NE::NodeId nodeId(HashId(node.path.GetString()));
|
||||
const ImVec2 livePos = NE::GetNodePosition(nodeId);
|
||||
const ImVec2 liveSize = NE::GetNodeSize(nodeId);
|
||||
if (livePos.x != FLT_MAX && liveSize.x > 0.0f) {
|
||||
r.pos = livePos;
|
||||
r.size = liveSize;
|
||||
}
|
||||
rects.push_back(r);
|
||||
}
|
||||
|
||||
const float margin = 24.0f;
|
||||
bool moved = true;
|
||||
while (moved) {
|
||||
moved = false;
|
||||
for (const Rect& r : rects) {
|
||||
const bool overlaps =
|
||||
desired.x < r.pos.x + r.size.x + margin && r.pos.x < desired.x + newSize.x + margin &&
|
||||
desired.y < r.pos.y + r.size.y + margin && r.pos.y < desired.y + newSize.y + margin;
|
||||
if (overlaps) {
|
||||
// March right past the blocker; x only grows, so the scan
|
||||
// terminates once it clears the rightmost overlapping node.
|
||||
desired.x = r.pos.x + r.size.x + margin;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return desired;
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos) {
|
||||
if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return;
|
||||
|
||||
@@ -1136,7 +1562,8 @@ void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const Im
|
||||
finalName = baseName + "_" + std::to_string(++suffix);
|
||||
|
||||
pxr::SdfPath path = m_materialPath.AppendChild(pxr::TfToken(finalName));
|
||||
pxr::GfVec2f pos(canvasPos.x, canvasPos.y);
|
||||
const ImVec2 freePos = FindFreeCanvasSpot(canvasPos, shaderId);
|
||||
pxr::GfVec2f pos(freePos.x, freePos.y);
|
||||
|
||||
m_commandHistory->Push(std::make_unique<CreateShaderNodeCommand>(m_stage, path, shaderId, pos));
|
||||
SyncFromUsd();
|
||||
@@ -1183,7 +1610,11 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
|
||||
// auto-layout). Only a real drag diverges from it and should author.
|
||||
for (const auto& node : m_graph.nodes) {
|
||||
if (node.path == path) {
|
||||
if (node.uiPosition == newValue) return;
|
||||
if (node.uiPosition == newValue) {
|
||||
LOG_INFO("[NodeGraph] position save for " + path.GetString() +
|
||||
" ignored (echo of snapshot seed)");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1193,7 +1624,17 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
|
||||
? oldValueVt.UncheckedGet<pxr::GfVec2f>()
|
||||
: pxr::GfVec2f(0.0f, 0.0f);
|
||||
|
||||
if (oldValue == newValue) return; // e.g. redundant Save call right after our own seed
|
||||
if (oldValue == newValue) {
|
||||
LOG_INFO("[NodeGraph] position save for " + path.GetString() +
|
||||
" ignored (customData already matches)");
|
||||
return;
|
||||
}
|
||||
|
||||
char moveMsg[256];
|
||||
std::snprintf(moveMsg, sizeof(moveMsg),
|
||||
"[NodeGraph] move committed: %s (%.1f, %.1f) -> (%.1f, %.1f)",
|
||||
path.GetText(), oldValue[0], oldValue[1], newValue[0], newValue[1]);
|
||||
LOG_INFO(std::string(moveMsg));
|
||||
|
||||
pxr::UsdStageRefPtr stage = m_stage;
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
|
||||
Reference in New Issue
Block a user