mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add NodeGraph pretty printing and fix connection traversal
This commit addresses three issues with NodeGraph handling: 1. **NodeGraph Pretty Printing**: Added NodeGraph to the type system to enable proper pretty printing instead of showing "VALUE_PPRINT: TODO: (type: NodeGraph)" error. Added NodeGraph to CASE_GPRIM_LIST macro in value-pprint.cc and implemented to_string() function in pprinter.cc. 2. **Duplicate Attribute Printing**: Fixed connection-only attributes being printed twice in NodeGraph and Shader prims. Added is_connection_only check in print_prop() to skip printing attribute declaration when it only has a connection and no value. 3. **NodeGraph Connection Traversal**: Fixed GetConnectedUVTexture() in render-data.cc to properly traverse through NodeGraph prims instead of treating their outputs as UsdUVTexture alpha channel (outputs:a). Key improvements: - Detect NodeGraph prims and extract their output connections - Fall back to Material children lookup when Stage indexing fails - Support matching prims by type when element names are empty - Continue traversal to find actual UsdUVTexture nodes This fixes the error: "connection Path's property part must be outputs:rgb/r/g/b for UsdUVTexture, but got outputs:a (prim_part: outputs:bnode_1_Separate_Color_Red_out)" Tested with: - test_nodegraph_float_output.usda (simplified test case) - web/js/assets/mtlx-test.usdz (original error case) Note: Full MaterialX shader chain traversal (through ND_extract_color3, ND_convert_* nodes) is not yet implemented and will be addressed in future work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1401,9 +1401,12 @@ std::string print_prop(const Property &prop, const std::string &prop_name,
|
||||
// timeSamples and connect cannot have attrMeta
|
||||
//
|
||||
|
||||
// Print attribute if it has metadata, has a value, OR is just typed
|
||||
// Print attribute if it has metadata, has a value, OR is just typed (but not connection-only)
|
||||
// NOTE: Some attributes (like outputs:out) may be typed but not have a value
|
||||
if (attr.metas().authored() || attr.has_value() || !attr.type_name().empty()) {
|
||||
// Skip printing declaration if this is a connection-only attribute (will be printed in the connection section below)
|
||||
bool is_connection_only = attr.has_connections() && !attr.has_value() && !attr.has_timesamples() && !attr.metas().authored();
|
||||
|
||||
if ((attr.metas().authored() || attr.has_value() || !attr.type_name().empty()) && !is_connection_only) {
|
||||
|
||||
ss << pprint::Indent(indent);
|
||||
|
||||
@@ -4209,6 +4212,38 @@ std::string to_string(const Shader &shader, const uint32_t indent,
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string to_string(const NodeGraph &nodegraph, const uint32_t indent,
|
||||
bool closing_brace) {
|
||||
std::stringstream ss;
|
||||
|
||||
ss << pprint::Indent(indent) << to_string(nodegraph.spec) << " NodeGraph \""
|
||||
<< nodegraph.name << "\"\n";
|
||||
if (nodegraph.meta.authored()) {
|
||||
ss << pprint::Indent(indent) << "(\n";
|
||||
ss << print_prim_metas(nodegraph.metas(), indent + 1);
|
||||
ss << pprint::Indent(indent) << ")\n";
|
||||
}
|
||||
ss << pprint::Indent(indent) << "{\n";
|
||||
|
||||
// NodeGraph-specific attributes
|
||||
if (nodegraph.nodedef.authored()) {
|
||||
ss << print_typed_attr(nodegraph.nodedef, "nodedef", indent + 1);
|
||||
}
|
||||
|
||||
if (nodegraph.nodegraph_type.authored()) {
|
||||
ss << print_typed_attr(nodegraph.nodegraph_type, "nodegraph_type", indent + 1);
|
||||
}
|
||||
|
||||
// Print properties (inputs, outputs, etc.)
|
||||
ss << print_props(nodegraph.props, indent + 1);
|
||||
|
||||
if (closing_brace) {
|
||||
ss << pprint::Indent(indent) << "}\n";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string to_string(const UsdPreviewSurface &surf, const uint32_t indent,
|
||||
bool closing_brace) {
|
||||
// TODO: Print spec and meta?
|
||||
|
||||
@@ -189,6 +189,9 @@ std::string to_string(const DomeLight::TextureFormat &texformat);
|
||||
std::string to_string(const Material &material, const uint32_t indent = 0,
|
||||
bool closing_brace = true);
|
||||
|
||||
std::string to_string(const NodeGraph &nodegraph, const uint32_t indent = 0,
|
||||
bool closing_brace = true);
|
||||
|
||||
// It will delegate to to_string() of concrete Shader type(e.g.
|
||||
// UsdPreviewSurface)
|
||||
std::string to_string(const Shader &shader, const uint32_t indent = 0,
|
||||
|
||||
@@ -1148,7 +1148,7 @@ nonstd::expected<VertexAttribute, std::string> GetTextureCoordinate(
|
||||
"\n");
|
||||
}
|
||||
|
||||
TUSDZ_LOG_I("get tex\n");
|
||||
//TUSDZ_LOG_I("get tex\n");
|
||||
// TODO: allow float2?
|
||||
if (primvar.get_type_id() !=
|
||||
value::TypeTraits<std::vector<value::texcoord2f>>::type_id()) {
|
||||
@@ -1157,10 +1157,10 @@ nonstd::expected<VertexAttribute, std::string> GetTextureCoordinate(
|
||||
primvar.get_type_name() + "\n");
|
||||
}
|
||||
|
||||
TUSDZ_LOG_I("flatten_with_indices\n");
|
||||
//TUSDZ_LOG_I("flatten_with_indices\n");
|
||||
std::vector<value::texcoord2f> uvs;
|
||||
if (!primvar.flatten_with_indices(t, &uvs, tinterp)) {
|
||||
TUSDZ_LOG_I("flatten_with_indices failed\n");
|
||||
//TUSDZ_LOG_I("flatten_with_indices failed\n");
|
||||
return nonstd::make_unexpected(
|
||||
"Failed to retrieve texture coordinate primvar with concrete type.\n");
|
||||
}
|
||||
@@ -1178,7 +1178,7 @@ nonstd::expected<VertexAttribute, std::string> GetTextureCoordinate(
|
||||
}
|
||||
|
||||
|
||||
TUSDZ_LOG_I("texcoord. " << name << ", " << uvs.size());
|
||||
//TUSDZ_LOG_I("texcoord. " << name << ", " << uvs.size());
|
||||
DCOUT("texcoord " << name << " : " << uvs);
|
||||
|
||||
vattr.format = VertexAttributeFormat::Vec2;
|
||||
@@ -1187,7 +1187,7 @@ nonstd::expected<VertexAttribute, std::string> GetTextureCoordinate(
|
||||
vattr.indices.clear(); // just in case.
|
||||
|
||||
vattr.name = name; // TODO: add "primvars:" namespace?
|
||||
TUSDZ_LOG_I("end");
|
||||
//TUSDZ_LOG_I("end");
|
||||
|
||||
return std::move(vattr);
|
||||
}
|
||||
@@ -2792,7 +2792,7 @@ bool RenderSceneConverter::BuildVertexIndicesImpl(RenderMesh &mesh) {
|
||||
// - Reorder vertex attributes to 'vertex' variability.
|
||||
//
|
||||
|
||||
TUSDZ_LOG_I("BuildVertexIndicesImpl");
|
||||
//TUSDZ_LOG_I("BuildVertexIndicesImpl");
|
||||
|
||||
const std::vector<uint32_t> &fvIndices =
|
||||
mesh.triangulatedFaceVertexIndices.size()
|
||||
@@ -3191,7 +3191,7 @@ bool RenderSceneConverter::BuildVertexIndicesFastImpl(RenderMesh &mesh) {
|
||||
// - Reorder vertex attributes to 'vertex' variability.
|
||||
//
|
||||
|
||||
TUSDZ_LOG_I("BuildVertexIndicesFastImpl");
|
||||
//TUSDZ_LOG_I("BuildVertexIndicesFastImpl");
|
||||
|
||||
const std::vector<uint32_t> &fvIndices =
|
||||
mesh.triangulatedFaceVertexIndices.size()
|
||||
@@ -3398,7 +3398,7 @@ bool RenderSceneConverter::BuildVertexIndicesFastImpl(RenderMesh &mesh) {
|
||||
|
||||
}
|
||||
|
||||
TUSDZ_LOG_I("proc normal");
|
||||
//TUSDZ_LOG_I("proc normal");
|
||||
|
||||
}
|
||||
|
||||
@@ -3431,7 +3431,7 @@ bool RenderSceneConverter::BuildVertexIndicesFastImpl(RenderMesh &mesh) {
|
||||
}
|
||||
|
||||
|
||||
TUSDZ_LOG_I("build indices");
|
||||
//TUSDZ_LOG_I("build indices");
|
||||
|
||||
// TODO: omit indices.
|
||||
std::vector<uint32_t> out_indices;
|
||||
@@ -3446,7 +3446,7 @@ bool RenderSceneConverter::BuildVertexIndicesFastImpl(RenderMesh &mesh) {
|
||||
mesh.usdFaceVertexIndices = std::move(out_indices);
|
||||
}
|
||||
|
||||
TUSDZ_LOG_I("done build indices");
|
||||
//TUSDZ_LOG_I("done build indices");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -3831,7 +3831,7 @@ bool RenderSceneConverter::ConvertMesh(
|
||||
if (ret) {
|
||||
const VertexAttribute &vattr = ret.value();
|
||||
|
||||
TUSDZ_LOG_I("uv attr");
|
||||
//TUSDZ_LOG_I("uv attr");
|
||||
|
||||
// Use slotId 0
|
||||
uvAttrs[0] = vattr;
|
||||
@@ -3898,7 +3898,7 @@ bool RenderSceneConverter::ConvertMesh(
|
||||
}
|
||||
}
|
||||
|
||||
TUSDZ_LOG_I("done uvAttr");
|
||||
//TUSDZ_LOG_I("done uvAttr");
|
||||
|
||||
if (mesh.has_primvar(env.mesh_config.default_tangents_primvar_name)) {
|
||||
GeomPrimvar pvar;
|
||||
@@ -4769,7 +4769,7 @@ bool RenderSceneConverter::ConvertMesh(
|
||||
(dst.binormals.empty() == 0 && dst.tangents.empty() == 0));
|
||||
|
||||
if (compute_normals || (compute_tangents && dst.normals.empty())) {
|
||||
TUSDZ_LOG_I("Build normals");
|
||||
//TUSDZ_LOG_I("Build normals");
|
||||
DCOUT("Compute normals");
|
||||
std::vector<vec3> normals;
|
||||
if (!ComputeNormals(dst.points, dst.faceVertexCounts(),
|
||||
@@ -4808,7 +4808,7 @@ bool RenderSceneConverter::ConvertMesh(
|
||||
if (env.mesh_config.build_vertex_indices && (!is_single_indexable)) {
|
||||
if (!env.mesh_config.prefer_non_indexed) {
|
||||
DCOUT("Build vertex indices");
|
||||
TUSDZ_LOG_I("Build vertex indices");
|
||||
//TUSDZ_LOG_I("Build vertex indices");
|
||||
|
||||
if (!BuildVertexIndicesFastImpl(dst)) {
|
||||
return false;
|
||||
@@ -4823,7 +4823,7 @@ bool RenderSceneConverter::ConvertMesh(
|
||||
//
|
||||
if (compute_tangents) {
|
||||
DCOUT("Compute tangents.");
|
||||
TUSDZ_LOG_I("Build tangents");
|
||||
//TUSDZ_LOG_I("Build tangents");
|
||||
|
||||
std::vector<vec2> texcoords;
|
||||
std::vector<vec3> normals;
|
||||
@@ -5196,35 +5196,219 @@ nonstd::expected<bool, std::string> GetConnectedUVTexture(
|
||||
constexpr auto kOutputsB = "outputs:b";
|
||||
constexpr auto kOutputsA = "outputs:a";
|
||||
|
||||
if (prop_part == kOutputsRGB) {
|
||||
// ok
|
||||
} else if (prop_part == kOutputsR) {
|
||||
// ok
|
||||
} else if (prop_part == kOutputsG) {
|
||||
// ok
|
||||
} else if (prop_part == kOutputsB) {
|
||||
// ok
|
||||
} else if (prop_part == kOutputsA) {
|
||||
// ok
|
||||
} else {
|
||||
return nonstd::make_unexpected(fmt::format(
|
||||
"connection Path's property part must be `{}`, `{}`, `{}` or `{}` "
|
||||
"for "
|
||||
"UsdUVTexture, but got `{}`\n",
|
||||
kOutputsRGB, kOutputsR, kOutputsG, kOutputsB, kOutputsA, prop_part));
|
||||
}
|
||||
TUSDZ_LOG_I("path: " << path);
|
||||
|
||||
// Check if prop_part is a standard UsdUVTexture output
|
||||
bool is_standard_output = (prop_part == kOutputsRGB) ||
|
||||
(prop_part == kOutputsR) ||
|
||||
(prop_part == kOutputsG) ||
|
||||
(prop_part == kOutputsB) ||
|
||||
(prop_part == kOutputsA);
|
||||
|
||||
const Prim *prim{nullptr};
|
||||
std::string err;
|
||||
if (!stage.find_prim_at_path(Path(prim_part, ""), prim, &err)) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Prim {} not found in the Stage: {}\n", prim_part, err));
|
||||
bool found_in_stage = stage.find_prim_at_path(Path(prim_part, ""), prim, &err);
|
||||
|
||||
// If not found in stage lookup, try to navigate through Material's children
|
||||
// This handles the case where NodeGraph is a child of Material but not in the Stage index
|
||||
if (!found_in_stage || !prim) {
|
||||
DCOUT("Prim not found in stage lookup, trying Material children approach");
|
||||
|
||||
// Extract Material path - it should be everything before the last element
|
||||
size_t last_slash = prim_part.rfind('/');
|
||||
if (last_slash == std::string::npos) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Prim {} not found in the Stage: {}\n", prim_part, err));
|
||||
}
|
||||
|
||||
std::string material_path = prim_part.substr(0, last_slash);
|
||||
std::string child_name = prim_part.substr(last_slash + 1);
|
||||
|
||||
DCOUT("Looking for Material at: " << material_path);
|
||||
DCOUT("Child name: " << child_name);
|
||||
|
||||
// Find the Material
|
||||
const Prim *mat_prim{nullptr};
|
||||
if (!stage.find_prim_at_path(Path(material_path, ""), mat_prim, &err)) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Prim {} not found (material lookup also failed): {}\n", prim_part, err));
|
||||
}
|
||||
|
||||
// Look for child prim
|
||||
if (mat_prim) {
|
||||
std::string children_info = "Material has " + std::to_string(mat_prim->children().size()) + " children: ";
|
||||
for (const auto& child : mat_prim->children()) {
|
||||
std::string elem_name = child.element_name();
|
||||
std::string child_type = child.data().type_name();
|
||||
children_info += "'" + elem_name + "'(" + child_type + ") ";
|
||||
|
||||
// Check by name match
|
||||
if (elem_name == child_name) {
|
||||
prim = &child;
|
||||
break;
|
||||
}
|
||||
// Also check if it's a NodeGraph/Shader by type name
|
||||
// This handles cases where element_name might not be set properly
|
||||
// e.g., looking for "NodeGraphs" and finding type "NodeGraph" with empty name
|
||||
if (child_type == "NodeGraph" && (child_name == "NodeGraphs" || child_name == "NodeGraph")) {
|
||||
prim = &child;
|
||||
break;
|
||||
}
|
||||
if (child_type == "Shader" && child_name == "Shader") {
|
||||
prim = &child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!prim) {
|
||||
DCOUT(children_info);
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Child prim '{}' not found in Material {}. {}\n", child_name, material_path, children_info));
|
||||
}
|
||||
} else {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Material prim {} is null\n", material_path));
|
||||
}
|
||||
}
|
||||
|
||||
if (!prim) {
|
||||
return nonstd::make_unexpected("[InternalError] Prim ptr is null.\n");
|
||||
}
|
||||
|
||||
// Check if this is a NodeGraph - if so, we need to traverse through it
|
||||
if (const NodeGraph *ng = prim->as<NodeGraph>()) {
|
||||
DCOUT("Connection goes through NodeGraph: " << prim_part);
|
||||
|
||||
// Look for the output property in the NodeGraph's props
|
||||
const auto &props = ng->props;
|
||||
auto it = props.find(prop_part);
|
||||
if (it == props.end()) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("NodeGraph {} does not have output property {}", prim_part, prop_part));
|
||||
}
|
||||
|
||||
const Property &output_prop = it->second;
|
||||
if (!output_prop.is_attribute()) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("NodeGraph output {} is not an attribute", prop_part));
|
||||
}
|
||||
|
||||
const Attribute &output_attr = output_prop.get_attribute();
|
||||
if (!output_attr.has_connections()) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("NodeGraph output {} has no connections", prop_part));
|
||||
}
|
||||
|
||||
// Get the connection from the NodeGraph output
|
||||
const auto &output_conns = output_attr.connections();
|
||||
if (output_conns.size() != 1) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("NodeGraph output {} must have exactly one connection, got {}",
|
||||
prop_part, output_conns.size()));
|
||||
}
|
||||
|
||||
const Path &ng_output_path = output_conns[0];
|
||||
DCOUT("NodeGraph output connects to: " << ng_output_path);
|
||||
|
||||
// Recursively follow the connection through the NodeGraph
|
||||
// We need to traverse to the next node in the chain
|
||||
std::string next_prim_part = ng_output_path.prim_part();
|
||||
std::string next_prop_part = ng_output_path.prop_part();
|
||||
|
||||
// Find the next prim in the chain
|
||||
// It might be a child of the NodeGraph, so use the same child lookup logic
|
||||
const Prim *next_prim{nullptr};
|
||||
bool found_next = stage.find_prim_at_path(Path(next_prim_part, ""), next_prim, &err);
|
||||
|
||||
// If not found in stage, it might be a child of the current NodeGraph
|
||||
if (!found_next || !next_prim) {
|
||||
DCOUT("Next prim not found in stage, checking NodeGraph children");
|
||||
|
||||
// Check if it's a child of this NodeGraph
|
||||
size_t last_slash = next_prim_part.rfind('/');
|
||||
if (last_slash != std::string::npos) {
|
||||
std::string parent_path = next_prim_part.substr(0, last_slash);
|
||||
std::string child_name = next_prim_part.substr(last_slash + 1);
|
||||
|
||||
// If the parent is this NodeGraph, look in its children
|
||||
if (parent_path == prim_part) {
|
||||
for (const auto& child : prim->children()) {
|
||||
std::string elem_name = child.element_name();
|
||||
if (elem_name == child_name) {
|
||||
next_prim = &child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!next_prim) {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Child prim '{}' not found in NodeGraph {}\n", child_name, prim_part));
|
||||
}
|
||||
} else {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Prim {} not found in the Stage: {}\n", next_prim_part, err));
|
||||
}
|
||||
} else {
|
||||
return nonstd::make_unexpected(
|
||||
fmt::format("Prim {} not found in the Stage: {}\n", next_prim_part, err));
|
||||
}
|
||||
}
|
||||
|
||||
if (!next_prim) {
|
||||
return nonstd::make_unexpected("[InternalError] next_prim is null.\n");
|
||||
}
|
||||
|
||||
// For nested NodeGraphs or other intermediate nodes, we would need to continue traversing
|
||||
// For now, we only support the common pattern: NodeGraph -> Shader(UsdUVTexture)
|
||||
// Nested NodeGraphs are rare and can be handled if needed
|
||||
|
||||
// Check if it's a Shader with UsdUVTexture
|
||||
if (const Shader *pshader = next_prim->as<Shader>()) {
|
||||
if (const UsdUVTexture *ptex = pshader->value.as<UsdUVTexture>()) {
|
||||
// Verify the property part is valid for UsdUVTexture
|
||||
if (next_prop_part != kOutputsRGB && next_prop_part != kOutputsR &&
|
||||
next_prop_part != kOutputsG && next_prop_part != kOutputsB &&
|
||||
next_prop_part != kOutputsA) {
|
||||
return nonstd::make_unexpected(fmt::format(
|
||||
"UsdUVTexture connection property part must be outputs:rgb/r/g/b/a, got {}",
|
||||
next_prop_part));
|
||||
}
|
||||
|
||||
DCOUT("Found UsdUVTexture through NodeGraph: " << next_prim_part);
|
||||
(*dst) = ptex;
|
||||
|
||||
if (shader_out) {
|
||||
(*shader_out) = pshader;
|
||||
}
|
||||
|
||||
if (tex_abs_path) {
|
||||
(*tex_abs_path) = ng_output_path;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Shader exists but it's not a UsdUVTexture - this is OK, NodeGraph might connect to other shader types
|
||||
// Return false (not found) rather than error
|
||||
DCOUT(fmt::format("NodeGraph {} output {} connects to Shader {} but it's not UsdUVTexture",
|
||||
prim_part, prop_part, next_prim_part));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we get here, the NodeGraph doesn't connect to a UsdUVTexture
|
||||
// This is not necessarily an error - the connection might be to a MaterialX shader or other node type
|
||||
DCOUT(fmt::format("NodeGraph {} output {} connects to {} (type: {}), not a UsdUVTexture",
|
||||
prim_part, prop_part, next_prim_part, next_prim->prim_type_name()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not a NodeGraph - must be a direct UsdUVTexture connection
|
||||
if (!is_standard_output) {
|
||||
return nonstd::make_unexpected(fmt::format(
|
||||
"connection Path's property part must be `{}`, `{}`, `{}`, `{}` or `{}` "
|
||||
"for UsdUVTexture, but got `{}`(prim_part: {}).",
|
||||
kOutputsRGB, kOutputsR, kOutputsG, kOutputsB, kOutputsA, prop_part, prim_part));
|
||||
}
|
||||
|
||||
if (tex_abs_path) {
|
||||
(*tex_abs_path) = Path(prim_part, prop_part);
|
||||
}
|
||||
@@ -6130,7 +6314,7 @@ bool RenderSceneConverter::ConvertUVTexture(const RenderSceneConverterEnv &env,
|
||||
}
|
||||
|
||||
} else {
|
||||
TUSDZ_LOG_I("get_value");
|
||||
//TUSDZ_LOG_I("get_value");
|
||||
Animatable<value::texcoord2f> fallbacks = texture.st.get_value();
|
||||
value::texcoord2f uv;
|
||||
if (fallbacks.get(env.timecode, &uv)) {
|
||||
@@ -6140,7 +6324,7 @@ bool RenderSceneConverter::ConvertUVTexture(const RenderSceneConverterEnv &env,
|
||||
// TODO: report warning.
|
||||
PUSH_WARN("Failed to get fallback `st` texcoord attribute.");
|
||||
}
|
||||
TUSDZ_LOG_I("uv done");
|
||||
//TUSDZ_LOG_I("uv done");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -779,7 +779,8 @@ namespace value {
|
||||
__FUNC(SkelAnimation) \
|
||||
__FUNC(BlendShape) \
|
||||
__FUNC(Material) \
|
||||
__FUNC(Shader)
|
||||
__FUNC(Shader) \
|
||||
__FUNC(NodeGraph)
|
||||
|
||||
#if 0 // remove
|
||||
// std::ostream &operator<<(std::ostream &os, const any_value &v) {
|
||||
|
||||
Reference in New Issue
Block a user