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:
2026-07-08 22:35:23 +08:00
parent 0f30e080e3
commit 038450a1ca
29 changed files with 13658 additions and 27 deletions
+450 -9
View File
@@ -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>(