Program Listing for File graph.cc#
↰ Return to documentation for file (src/graph.cc)
#include <algorithm>
#include <vector>
#include "nodes-graph/graph.h"
#include "nodes-graph/node_definition.h"
#include "nodes-graph/graph_manager.h"
namespace ng {
Graph::Graph(
std::string name,
nodes_t nodes,
links_t links,
input_sockets_t input_sockets,
output_sockets_t output_sockets,
interface_t parameters,
interface_t results
): name_(std::move(name)),
input_sockets_(std::move(input_sockets)),
output_sockets_(std::move(output_sockets)),
nodes_(std::move(nodes)),
links_(std::move(links)),
parameters_(std::move(parameters)),
results_(std::move(results))
{}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Nodes *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
node_id_t Graph::make_unique_node_id(const qualifier_t& q) const {
node_id_t node_id;
do {
node_id = as_node_id(q, Node::make_random_id());
} while (has_node(node_id));
return node_id;
}
node_id_t Graph::instantiate(const NodeDefinition& nd) {
node_id_t node_id = make_unique_node_id(nd.get_qualifier());
Node node(nd.get_default_name(), node_id, nd.get_qualifier());
for (const InputSocket& s : nd.get_input_sockets()) {
input_sockets_[as_socket_uuid(node_id, s.get_id())] = s;
node.register_input_socket(s.get_id());
}
for (const OutputSocket& s : nd.get_output_sockets()) {
output_sockets_[as_socket_uuid(node_id, s.get_id())] = s;
node.register_output_socket(s.get_id());
}
nodes_.emplace(node_id, std::move(node));
return node_id;
}
node_id_t Graph::add_node(const qualifier_t& node_qualifier) {
const NodesRegistry& registry = NodesRegistry::instance();
auto it = registry.find(node_qualifier);
if (it == registry.end()) { return null_node; }
return instantiate(it->second);
}
node_id_t Graph::add_node(const char* node_qualifier) {
return add_node(as_qualifier(node_qualifier));
}
Node& Graph::get_node(const node_id_t& node_id) {
return nodes_.at(node_id);
}
const Node& Graph::get_node(const node_id_t& node_id) const {
return nodes_.at(node_id);
}
void Graph::rename_node(const node_id_t& node_id, const std::string& name) {
get_node(node_id).set_name(name);
}
bool Graph::remove_node(const node_id_t& node_id) {
if (!has_node(node_id)) { return false; }
disconnect_node(node_id, LinkBreakReason::NodeRemoved);
auto lo_in = input_sockets_.lower_bound(as_socket_uuid(node_id, null_socket));
auto hi_in = input_sockets_.upper_bound(as_socket_uuid(node_id, inf_socket));
for (auto it = lo_in; it != hi_in; ++it) {
parameters_.erase(it->first);
}
input_sockets_.erase(lo_in, hi_in);
auto lo_out = output_sockets_.lower_bound(as_socket_uuid(node_id, null_socket));
auto hi_out = output_sockets_.upper_bound(as_socket_uuid(node_id, inf_socket));
for (auto it = lo_out; it != hi_out; ++it) {
results_.erase(it->first);
}
output_sockets_.erase(lo_out, hi_out);
nodes_.erase(node_id);
return true;
}
void Graph::disconnect_node(const node_id_t& node_id, LinkBreakReason reason) {
// Collected first: callbacks are allowed to modify the links.
std::vector<link_t> to_break;
for (const link_t& link : links_) {
if (link.first.first == node_id || link.second.first == node_id) {
to_break.push_back(link);
}
}
for (const link_t& link : to_break) {
break_link(links_.find(link), reason);
}
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Sockets *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const InputSocket& Graph::get_input_socket(const socket_uuid_t& suuid) const {
return input_sockets_.at(suuid);
}
const OutputSocket& Graph::get_output_socket(const socket_uuid_t& suuid) const {
return output_sockets_.at(suuid);
}
Status Graph::set_socket_value(const socket_uuid_t& suuid, const std::string& value) {
auto it = input_sockets_.find(suuid);
if (it == input_sockets_.end()) { return Status::NoInputSocket; }
InputSocket& socket = it->second;
if (!socket.is_allowed_value(value)) { return Status::NotAllowedValue; }
if (!socket.check_validity(value)) { return Status::InvalidValue; }
std::string old_value = socket.get_value();
socket.set_value(value);
socket.trigger_on_value_changed_callbacks(*this, get_node(suuid.first), old_value);
return Status::Ok;
}
const std::string& Graph::get_socket_value(const socket_uuid_t& suuid) const {
return input_sockets_.at(suuid).get_value();
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Links *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Graph::links_t::const_iterator Graph::find_link_by_input_socket(const socket_uuid_t& target) const {
// Links are sorted by destination first, the smallest possible source gives the first candidate.
link_t lowest = as_link(as_socket_uuid(null_node, null_socket), target);
auto it = links_.lower_bound(lowest);
if (it != links_.end() && it->first == target) {
return it;
}
return links_.end();
}
Status Graph::add_link(const socket_uuid_t& from, const socket_uuid_t& to) {
return add_link(as_link(from, to));
}
Status Graph::add_link(const link_t& link) {
if (links_.count(link) > 0) { return Status::Ok; }
auto it_from = output_sockets_.find(link.second);
if (it_from == output_sockets_.end()) { return Status::NoOutputSocket; }
auto it_to = input_sockets_.find(link.first);
if (it_to == input_sockets_.end()) { return Status::NoInputSocket; }
if (!it_from->second.accepts_type(it_to->second.get_type())) { return Status::IncompatibleTypes; }
if (!it_to->second.is_linkable()) { return Status::NotLinkable; }
// Checked before replacing the existing link, so a refused link leaves the graph untouched.
// Keeping the replaced link in the check can't create a false positive: a simple cycle enters a socket only once.
if (causes_cycle(link)) { return Status::CausesCycle; }
break_link(find_link_by_input_socket(link.first), LinkBreakReason::Replaced);
parameters_.erase(link.first);
links_.insert(link);
Node& node_from = get_node(link.second.first);
Node& node_to = get_node(link.first.first);
it_from->second.trigger_on_added_link_callbacks(*this, node_from, node_to, it_to->second);
it_to->second.trigger_on_added_link_callbacks(*this, node_from, it_from->second, node_to);
return Status::Ok;
}
bool Graph::break_link(links_t::const_iterator link_it, LinkBreakReason reason) {
if (link_it == links_.end()) { return false; }
link_t link = *link_it;
links_.erase(link_it);
InputSocket& socket_to = input_sockets_.at(link.first);
OutputSocket& socket_from = output_sockets_.at(link.second);
Node& node_from = get_node(link.second.first);
Node& node_to = get_node(link.first.first);
socket_from.trigger_on_broken_link_callbacks(*this, node_from, node_to, socket_to, reason);
socket_to.trigger_on_broken_link_callbacks(*this, node_from, socket_from, node_to, reason);
return true;
}
bool Graph::break_link(const link_t& link) {
return break_link(links_.find(link), LinkBreakReason::Manual);
}
Graph::links_t Graph::get_input_links(const node_id_t& node_id) const {
links_t result;
for (const link_t& link : links_) {
if (link.first.first == node_id) {
result.insert(link);
}
}
return result;
}
Graph::links_t Graph::get_output_links(const node_id_t& node_id) const {
links_t result;
for (const link_t& link : links_) {
if (link.second.first == node_id) {
result.insert(link);
}
}
return result;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Interface *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
bool Graph::is_alias_available(const interface_t& interface, const socket_uuid_t& suuid, const socket_id_t& alias) {
for (const auto& [other, ppts] : interface) {
if (other != suuid && ppts.socket_id == alias) {
return false;
}
}
return true;
}
Status Graph::expose_parameter(const socket_uuid_t& suuid, const socket_id_t& alias, std::string show_as) {
auto it = input_sockets_.find(suuid);
if (it == input_sockets_.end()) { return Status::NoInputSocket; }
if (!is_alias_available(parameters_, suuid, alias)) { return Status::DuplicateAlias; }
if (show_as.empty()) {
show_as = it->second.get_name();
}
break_link(find_link_by_input_socket(suuid), LinkBreakReason::Replaced);
parameters_[suuid] = InterfaceProperties{std::move(show_as), alias};
return Status::Ok;
}
bool Graph::remove_parameter(const socket_uuid_t& suuid) {
return parameters_.erase(suuid) > 0;
}
Status Graph::expose_result(const socket_uuid_t& suuid, const socket_id_t& alias, std::string show_as) {
auto it = output_sockets_.find(suuid);
if (it == output_sockets_.end()) { return Status::NoOutputSocket; }
if (!is_alias_available(results_, suuid, alias)) { return Status::DuplicateAlias; }
if (show_as.empty()) {
show_as = it->second.get_name();
}
results_[suuid] = InterfaceProperties{std::move(show_as), alias};
return Status::Ok;
}
bool Graph::remove_result(const socket_uuid_t& suuid) {
return results_.erase(suuid) > 0;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Proxies *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
node_id_t Graph::add_proxy_node(const qualifier_t& graph_qualifier) {
if (qualifier_.has_value() && qualifier_.value() == graph_qualifier) { return null_node; }
const Graph g = GraphManager::instance().raw_read(graph_qualifier);
node_id_t node_id = make_unique_node_id(graph_qualifier);
Node proxy(g.get_name(), node_id, graph_qualifier);
proxy.set_proxy(true);
for (const auto& [suuid, ppts] : g.get_parameters()) {
InputSocket s = g.get_input_socket(suuid);
s.set_alias(ppts.show_as, ppts.socket_id);
input_sockets_[as_socket_uuid(node_id, s.get_id())] = s;
proxy.register_input_socket(s.get_id());
}
for (const auto& [suuid, ppts] : g.get_results()) {
OutputSocket s = g.get_output_socket(suuid);
s.set_alias(ppts.show_as, ppts.socket_id);
output_sockets_[as_socket_uuid(node_id, s.get_id())] = s;
proxy.register_output_socket(s.get_id());
}
nodes_.emplace(node_id, std::move(proxy));
return node_id;
}
node_id_t Graph::add_proxy_node(const char* graph_qualifier) {
return add_proxy_node(as_qualifier(graph_qualifier));
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Topology *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
std::set<node_id_t> Graph::get_source_nodes() const {
return get_source_nodes(links_);
}
std::set<node_id_t> Graph::get_source_nodes(const links_t& links) const {
std::set<node_id_t> source_nodes;
for (const auto& [node_id, _] : nodes_) {
source_nodes.insert(node_id);
}
for (const link_t& link : links) {
source_nodes.erase(link.first.first);
}
return source_nodes;
}
bool Graph::causes_cycle(const link_t& link) const {
links_t candidate_links = links_;
candidate_links.insert(link);
return get_depth_properties(candidate_links).has_cycle;
}
Graph::DepthProperties Graph::get_depth_properties() const {
return get_depth_properties(links_);
}
Graph::DepthProperties Graph::get_depth_properties(const links_t& links) const {
DepthProperties props;
// Number of incoming links not processed yet, for each node.
SmallMap<node_id_t, std::size_t> in_degree;
in_degree.reserve(nodes_.size());
for (const auto& [node_id, _] : nodes_) {
in_degree[node_id] = 0;
}
// Successors of each node (links are stored as {to, from}).
std::map<node_id_t, std::vector<node_id_t>> successors;
for (const link_t& link : links) {
in_degree[link.first.first]++;
successors[link.second.first].push_back(link.first.first);
}
std::vector<node_id_t> stack;
stack.reserve(nodes_.size());
props.depth.reserve(nodes_.size());
for (const auto& [node_id, count] : in_degree) {
if (count == 0) {
stack.push_back(node_id);
props.depth[node_id] = 0;
}
}
std::size_t processed = 0;
while (!stack.empty()) {
node_id_t current = stack.back();
stack.pop_back();
processed++;
std::size_t current_depth = props.depth[current];
props.max_depth = std::max(props.max_depth, current_depth);
auto it = successors.find(current);
if (it == successors.end()) { continue; }
for (const node_id_t& next : it->second) {
std::size_t& d = props.depth[next];
d = std::max(d, current_depth + 1);
if (--in_degree[next] == 0) {
stack.push_back(next);
}
}
}
props.has_cycle = processed < in_degree.size();
return props;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * Deep comparison *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
namespace {
// Compares two maps sharing the same key ordering: keys must be equal and values equal according to `eq`.
template <typename Map, typename Eq>
bool same_maps(const Map& a, const Map& b, Eq eq) {
if (a.size() != b.size()) { return false; }
auto it_b = b.begin();
for (const auto& [key, value] : a) {
if (key != it_b->first || !eq(value, it_b->second)) { return false; }
++it_b;
}
return true;
}
} // namespace
bool CmpGraph::same_socket(const Socket& a, const Socket& b) const {
return a.get_name() == b.get_name()
&& a.get_id() == b.get_id()
&& a.get_type() == b.get_type()
&& a.is_active() == b.is_active()
&& a.get_on_added_link_callbacks() == b.get_on_added_link_callbacks()
&& a.get_on_broken_link_callbacks() == b.get_on_broken_link_callbacks();
}
bool CmpGraph::same_input_socket(const InputSocket& a, const InputSocket& b) const {
return same_socket(a, b)
&& a.get_value() == b.get_value()
&& a.is_linkable() == b.is_linkable()
&& a.get_allowed_values() == b.get_allowed_values()
&& a.get_on_value_changed_callbacks() == b.get_on_value_changed_callbacks();
}
bool CmpGraph::same_output_socket(const OutputSocket& a, const OutputSocket& b) const {
return same_socket(a, b);
}
bool CmpGraph::same_node(const Node& a, const Node& b) const {
return a.get_name() == b.get_name()
&& a.get_id() == b.get_id()
&& a.get_qualifier() == b.get_qualifier()
&& a.is_proxy() == b.is_proxy()
&& a.get_input_sockets() == b.get_input_sockets()
&& a.get_output_sockets() == b.get_output_sockets();
}
bool CmpGraph::same_interface(const Graph::InterfaceProperties& a, const Graph::InterfaceProperties& b) const {
return a.show_as == b.show_as && a.socket_id == b.socket_id;
}
bool CmpGraph::equal(const Graph& a, const Graph& b) const {
if (&a == &b) { return true; }
return a.get_name() == b.get_name()
&& a.get_qualifier() == b.get_qualifier()
&& a.get_links() == b.get_links()
&& same_maps(a.get_nodes(), b.get_nodes(),
[this](const Node& x, const Node& y) { return same_node(x, y); })
&& same_maps(a.get_input_sockets(), b.get_input_sockets(),
[this](const InputSocket& x, const InputSocket& y) { return same_input_socket(x, y); })
&& same_maps(a.get_output_sockets(), b.get_output_sockets(),
[this](const OutputSocket& x, const OutputSocket& y) { return same_output_socket(x, y); })
&& same_maps(a.get_parameters(), b.get_parameters(),
[this](const Graph::InterfaceProperties& x, const Graph::InterfaceProperties& y) { return same_interface(x, y); })
&& same_maps(a.get_results(), b.get_results(),
[this](const Graph::InterfaceProperties& x, const Graph::InterfaceProperties& y) { return same_interface(x, y); });
}
} // namespace ng