Skip to content

Instantly share code, notes, and snippets.

@sthairno
Created August 2, 2020 07:34
Show Gist options
  • Save sthairno/98b76ab35f95d3792c172374de20d6b9 to your computer and use it in GitHub Desktop.
Save sthairno/98b76ab35f95d3792c172374de20d6b9 to your computer and use it in GitHub Desktop.
S3D_NodeEditor
#include<Siv3D.hpp>
#include<any>
#include<typeinfo>
#include"SasaGUI.hpp"
/// <summary>
/// type_infoをコピー可能にするラッパクラス
/// </summary>
class Type
{
private:
const type_info* m_typeInfo;
public:
Type(const type_info& type)
:m_typeInfo(&type)
{
}
String getName() const
{
return Unicode::FromUTF8(m_typeInfo->name());
}
const type_info& TypeInfo() const
{
return *m_typeInfo;
}
bool operator==(const Type& other) const
{
return *m_typeInfo == *other.m_typeInfo;
}
bool operator!=(const Type& other) const
{
return *m_typeInfo != *other.m_typeInfo;
}
template<class T>
static Type getType()
{
return Type(typeid(T));
}
static Type getType(const std::any& val)
{
return Type(val.type());
}
};
namespace NodeEditor
{
struct Config
{
float WidthMin = 100;
float TitleHeight = 20;
float RectR = 5;
float ConnectorSize = 10;
float BezierX = 50;
Font font = Font(16);
std::map<size_t, Texture> typeIconList = std::map<size_t, Texture>
{
{typeid(Image).hash_code(),Texture(Icon::CreateImage(0xf03e,10).negate())},
{typeid(int).hash_code(),Texture(Image(10,10,Palette::Aqua))},
{typeid(double).hash_code(),Texture(Image(10,10,Palette::Blue))}
};
const Optional<Texture> getTypeIcon(const Type& type) const
{
const auto itr = typeIconList.find(type.TypeInfo().hash_code());
if (itr == typeIconList.end())
{
return none;
}
else
{
return itr->second;
}
}
};
class INode;
class NodeSocket
{
private:
INode& m_node;
const Type m_valueType;
const String m_name;
std::any m_value;
public:
const size_t Index;
std::shared_ptr<NodeSocket> ConnectedSocket;
NodeSocket(INode& node, const size_t index, Type type, const String& desc)
:m_node(node),
Index(index),
m_valueType(type),
m_name(desc)
{
}
String name() const
{
return m_name;
}
Type type() const
{
return m_valueType;
}
std::any value() const
{
return m_value;
}
INode& node() const
{
return m_node;
}
void setValue(std::any value)
{
if (!value.has_value() || value.type() != m_valueType.TypeInfo())
{
throw std::exception();
}
m_value = value;
}
};
class INode
{
private:
SizeF m_size;
bool m_isGrab = false;
Array<std::shared_ptr<NodeSocket>> m_inputSockets;
Array<std::shared_ptr<NodeSocket>> m_outputSockets;
Stopwatch m_lastCallStw = Stopwatch(true);
void calcSize(const Config& cfg)
{
m_size.y = Max(m_inputSockets.size(), m_outputSockets.size()) * cfg.font.height() + cfg.TitleHeight + cfg.RectR + ChildSize.y;
float inWidthMax = 0, outWidthMax = 0;
for (auto& inSocket : m_inputSockets)
{
const auto tex = cfg.getTypeIcon(inSocket->type());
const float width = static_cast<float>(cfg.font(inSocket->name()).region().x + (tex ? tex->width() : 0));
inWidthMax = Max(inWidthMax, width);
}
for (auto& outSocket : m_outputSockets)
{
const auto tex = cfg.getTypeIcon(outSocket->type());
const float width = static_cast<float>(cfg.font(outSocket->name()).region().x + (tex ? tex->width() : 0));
outWidthMax = Max(outWidthMax, width);
}
m_size.x = Max<float>({ 100, inWidthMax + outWidthMax, (float)ChildSize.x });
}
protected:
SizeF ChildSize;
virtual void childCalc() = 0;
virtual void childDraw(const Config&) = 0;
template<class T>
void setOutput(const size_t index, const T& input)
{
m_outputSockets[index]->setValue(std::any(input));
}
template<class T>
T getInput(const size_t index) const
{
return std::any_cast<T>(m_inputSockets[index]->value());
}
void cfgInputSockets(Array<std::pair<Type, String>> cfg)
{
m_inputSockets = Array<std::shared_ptr<NodeSocket>>(cfg.size());
for (size_t i = 0; i < cfg.size(); i++)
{
m_inputSockets[i] = std::make_shared<NodeSocket>(*this, i, cfg[i].first, cfg[i].second);
}
}
void cfgOutputSockets(Array<std::pair<Type, String>> cfg)
{
m_outputSockets = Array<std::shared_ptr<NodeSocket>>(cfg.size());
for (size_t i = 0; i < cfg.size(); i++)
{
m_outputSockets[i] = std::make_shared<NodeSocket>(*this, i, cfg[i].first, cfg[i].second);
}
}
public:
Vec2 Location = Vec2(0, 0);
String Name;
INode()
{
}
void calc()
{
for (auto& inSocket : m_inputSockets)
{
if (!inSocket->ConnectedSocket)
{
throw std::exception(U"ノード名:\"{}\", \"入力ソケット:\"{}\"のノードが指定されていません"_fmt(Name, inSocket->name()).toUTF8().c_str());
}
inSocket->ConnectedSocket->node().calc();
inSocket->setValue(inSocket->ConnectedSocket->value());
}
m_lastCallStw.restart();
childCalc();
for (auto& outSocket : m_outputSockets)
{
if (!outSocket->value().has_value())
{
throw std::exception(U"ノード名:\"{}\", \"出力ソケット:\"{}\"の出力が指定されていません"_fmt(Name, outSocket->name()).toUTF8().c_str());
}
}
}
void draw(Config& cfg)
{
calcSize(cfg);
if (m_isGrab)
{
if (MouseL.pressed())
{
Location += Cursor::DeltaF();
Cursor::RequestStyle(CursorStyle::Hand);
}
else
{
m_isGrab = false;
}
}
const auto rect = RectF(Location, m_size);
const auto titleRect = RectF(Location, m_size.x, cfg.TitleHeight);
const auto inRect = RectF(rect.x, rect.y + cfg.TitleHeight, rect.w, rect.h - cfg.TitleHeight - cfg.RectR);
auto childRect = RectF(inRect.x, inRect.y + inRect.h - ChildSize.x, inRect.w, ChildSize.y);
if (titleRect.mouseOver())
{
Cursor::RequestStyle(CursorStyle::Hand);
if (MouseL.down())
{
m_isGrab = true;
}
}
const auto& roundRect = rect.rounded(cfg.RectR);
if (m_lastCallStw.elapsed() < 1s)
{
roundRect.drawShadow({ 0,0 }, 10, 5 * (1 - m_lastCallStw.elapsed() / 1s), Palette::Red);
}
roundRect.draw(ColorF(0.7));
inRect.draw(ColorF(0.9));
for (int i = 0; i < m_inputSockets.size(); i++)
{
auto& inSocket = m_inputSockets[i];
const auto tex = cfg.getTypeIcon(inSocket->type());
const Vec2 fontPos = inRect.tl() + Vec2(0, cfg.font.height() * i) + Vec2(tex ? tex->width() : 0, 0);
auto fontlc = cfg.font(inSocket->name()).draw(Arg::topLeft = fontPos, Palette::Black).leftCenter();
if (tex)
{
tex->draw(Arg::rightCenter = fontlc);
}
auto pos = calcInSocketPos(i, cfg);
auto circle = Circle(pos, cfg.ConnectorSize / 2);
if (inSocket->ConnectedSocket)
{
circle.draw(Palette::White);
Vec2 otherOutSocketPos = inSocket->ConnectedSocket->node().calcOutSocketPos(inSocket->ConnectedSocket->Index, cfg);
Bezier3(otherOutSocketPos, otherOutSocketPos + Vec2(cfg.BezierX, 0), pos + Vec2(-cfg.BezierX, 0), pos).draw(4, Palette::White);
}
else
{
circle.drawFrame(1, Palette::White);
}
}
for (int i = 0; i < m_outputSockets.size(); i++)
{
auto& outSocket = m_outputSockets[i];
const auto tex = cfg.getTypeIcon(outSocket->type());
const Vec2 fontPos = inRect.tr() + Vec2(0, cfg.font.height() * i) - Vec2(tex ? tex->width() : 0, 0);
const auto fontrc = cfg.font(outSocket->name()).draw(Arg::topRight = fontPos, Palette::Black).rightCenter();
if (tex)
{
tex->draw(Arg::leftCenter = fontrc);
}
auto circle = Circle(calcOutSocketPos(i, cfg), cfg.ConnectorSize / 2);
if (outSocket->ConnectedSocket)
{
circle.draw(Palette::White);
//Vec2 otherOutSocketPos = getOutSocketPos(*node->PrevNode);
//Bezier3(otherOutSocketPos, otherOutSocketPos + Vec2(100, 0), inSocketPos + Vec2(-100, 0), inSocketPos).draw(4, Palette::White);
}
else
{
circle.drawFrame(1, Palette::White);
}
}
cfg.font(Name).drawAt(titleRect.center(), Palette::Black);
if (ChildSize != SizeF(0, 0))
{
const Transformer2D transform(Mat3x2::Translate(childRect.pos), true);
/*RasterizerState rasterizerState = Graphics2D::GetRasterizerState();
rasterizerState.scissorEnable = true;
ScopedRenderStates2D renderState(rasterizerState);
Graphics2D::SetScissorRect(childRect);*/
childDraw(cfg);
}
}
template<class T>
void setInput(const size_t idx, const T& input)
{
auto& inSocket = m_inputSockets[idx];
if (inSocket->type() != Type::getType<T>())
{
throw std::exception(String(U"入力できない型の値を入力しました").toUTF8().c_str());
}
inSocket->value(std::any(input));
}
template<class T>
T getOutput(const size_t idx)
{
auto& outSocket = m_outputSockets[idx];
if (outSocket->type() == Type::getType<void>())
{
throw std::exception(String(U"このノードの出力はありません").toUTF8().c_str());
}
if (!outSocket->value().has_value())
{
calc();
}
return std::any_cast<T>(outSocket->value());
}
const Array<std::shared_ptr<NodeSocket>> getInputSockets() const
{
return m_inputSockets;
}
const Array<std::shared_ptr<NodeSocket>> getOutputSockets() const
{
return m_outputSockets;
}
Vec2 calcInSocketPos(const size_t idx, const Config& cfg)
{
return RectF(Location, m_size).tl() + Vec2(-cfg.ConnectorSize / 2, cfg.TitleHeight + cfg.font.height() * (idx + 0.5f));
}
Vec2 calcOutSocketPos(const size_t idx, const Config& cfg)
{
return RectF(Location, m_size).tr() + Vec2(cfg.ConnectorSize / 2, cfg.TitleHeight + cfg.font.height() * (idx + 0.5f));
}
};
class NodeEditor
{
private:
enum class GrabTarget
{
None, Output, Input
};
std::map<uint32, std::shared_ptr<INode>> m_nodelist;
uint32 m_nextId = 1;
GrabTarget m_grabTarget = GrabTarget::None;
std::shared_ptr<NodeSocket> m_grabFrom;
RenderTexture m_texture;
Config m_config;
public:
NodeEditor(Size size)
{
resize(size);
}
void resize(Size size)
{
m_texture = RenderTexture(size);
}
/// <summary>
/// 描画,更新処理
/// </summary>
/// <param name="drawArea">エディタを表示する範囲</param>
void draw(const Vec2 location)
{
{
const ScopedRenderTarget2D renderTarget(m_texture);
const Transformer2D transform(Mat3x2::Identity(), Mat3x2::Translate(location));
Scene::Rect().draw(ColorF(0.4));
//ノードの描画、ノード接続の編集
for (auto& keyvalue : m_nodelist)
{
auto node = keyvalue.second;
//ノード描画
node->draw(m_config);
}
//ノードの接続編集
{
std::shared_ptr<NodeSocket> candidateSocket;//接続先の候補(見つからないときはnullptr)
for (auto& keyvalue : m_nodelist)
{
auto node = keyvalue.second;
//入力ソケット
for (auto& inSocket : node->getInputSockets())
{
auto pos = node->calcInSocketPos(inSocket->Index, m_config);
auto circle = Circle(pos, m_config.ConnectorSize / 2);
if (circle.leftClicked())
{
if (inSocket->ConnectedSocket)
{
inSocket->ConnectedSocket->ConnectedSocket = nullptr;
inSocket->ConnectedSocket = nullptr;
}
m_grabTarget = GrabTarget::Output;
m_grabFrom = inSocket;
}
if (m_grabTarget == GrabTarget::Input && m_grabFrom->type() == inSocket->type() && &m_grabFrom->node() != &*node)
{
if (circle.mouseOver())
{
candidateSocket = inSocket;
}
}
}
//出力ソケット
for (auto& outSocket : node->getOutputSockets())
{
auto pos = node->calcOutSocketPos(outSocket->Index, m_config);
auto circle = Circle(pos, m_config.ConnectorSize / 2);
if (circle.leftClicked())
{
if (outSocket->ConnectedSocket)
{
outSocket->ConnectedSocket->ConnectedSocket = nullptr;
outSocket->ConnectedSocket = nullptr;
}
m_grabTarget = GrabTarget::Input;
m_grabFrom = outSocket;
}
if (m_grabTarget == GrabTarget::Output && m_grabFrom->type() == outSocket->type() && &m_grabFrom->node() != &*node)
{
if (circle.mouseOver())
{
candidateSocket = outSocket;
}
}
}
}
if (m_grabTarget != GrabTarget::None)
{
Vec2 start, end;
switch (m_grabTarget)
{
case GrabTarget::Output:
{
start = candidateSocket ? candidateSocket->node().calcOutSocketPos(candidateSocket->Index, m_config) : Cursor::PosF();
end = m_grabFrom->node().calcInSocketPos(m_grabFrom->Index, m_config);
}
break;
case GrabTarget::Input:
{
start = m_grabFrom->node().calcOutSocketPos(m_grabFrom->Index, m_config);
end = candidateSocket ? candidateSocket->node().calcInSocketPos(candidateSocket->Index, m_config) : Cursor::PosF();
}
break;
}
Bezier3(start, start + Vec2(m_config.BezierX, 0), end + Vec2(-m_config.BezierX, 0), end).draw(4, Palette::White);
if (!MouseL.pressed())
{
if (candidateSocket)
{
m_grabFrom->ConnectedSocket = candidateSocket;
if (candidateSocket->ConnectedSocket)
{
candidateSocket->ConnectedSocket->ConnectedSocket = nullptr;
candidateSocket->ConnectedSocket = nullptr;
}
candidateSocket->ConnectedSocket = m_grabFrom;
}
m_grabTarget = GrabTarget::None;
}
}
}
}
m_texture.draw(location);
}
void addNode(std::shared_ptr<INode> node, const Vec2& pos = Vec2(0, 0))
{
m_nodelist[m_nextId++] = node;
node->Location = pos;
}
};
}
class IncrementNode : public NodeEditor::INode
{
private:
void childCalc() override
{
auto val = getInput<int>(0);
setOutput(0, val + 1);
}
void childDraw(const NodeEditor::Config&) override
{
}
public:
IncrementNode()
{
cfgInputSockets({ {Type::getType<int>(),U"A"} });
cfgOutputSockets({ {Type::getType<int>(),U"A+1"} });
ChildSize = SizeF(0, 0);
Name = U"Inc";
}
};
class AddNode : public NodeEditor::INode
{
private:
void childCalc() override
{
auto valA = getInput<int>(0);
auto valB = getInput<int>(1);
setOutput(0, valA + valB);
}
void childDraw(const NodeEditor::Config&) override
{
//font(U"+1").draw(0, 0, Palette::Black);
}
public:
AddNode()
{
cfgInputSockets({ {Type::getType<int>(),U"A"},{Type::getType<int>(),U"B"} });
cfgOutputSockets({ {Type::getType<int>(),U"A+B"} });
ChildSize = SizeF(0, 0);
Name = U"Add";
}
};
class IntegerNode : public NodeEditor::INode
{
private:
void childCalc() override
{
setOutput(0, Value);
}
void childDraw(const NodeEditor::Config& cfg) override
{
if (KeyW.down())Value++;
if (KeyS.down())Value--;
cfg.font(Value).draw(0, 0, Palette::Black);
}
public:
int Value = Random(-100, 100);
IntegerNode()
{
cfgInputSockets({ });
cfgOutputSockets({ {Type::getType<int>(),U"Val"} });
ChildSize = SizeF(20, 20);
Name = U"Int32";
}
};
class PreviewNode : public NodeEditor::INode
{
private:
int result = 0;
void childCalc() override
{
result = getInput<int>(0);
}
void childDraw(const NodeEditor::Config& cfg) override
{
String text;
try
{
calc();
text = Format(result);
}
catch (std::exception ex)
{
text = Unicode::FromUTF8(ex.what());
}
cfg.font(text).draw(0, 0, Palette::Black);
}
public:
PreviewNode()
{
cfgInputSockets({ {Type::getType<int>(),U"Val"} });
cfgOutputSockets({ });
ChildSize = SizeF(20, 20);
Name = U"Preview";
}
};
class RealNode : public NodeEditor::INode
{
private:
void childCalc() override
{
setOutput(0, Value);
}
void childDraw(const NodeEditor::Config& cfg) override
{
if (KeyW.down())Value++;
if (KeyS.down())Value--;
cfg.font(Value).draw(0, 0, Palette::Black);
}
public:
double Value = Random(-100, 100);
RealNode()
{
cfgInputSockets({ });
cfgOutputSockets({ {Type::getType<double>(),U"Val"} });
ChildSize = SizeF(20, 20);
Name = U"double";
}
};
void Main()
{
NodeEditor::NodeEditor editor(Scene::Size() - Size(100, 100));
SasaGUI::GUIManager gui;
bool windowVisible = false;
Vec2 windowPos(0, 0);
while (System::Update())
{
gui.frameBegin();
//右クリックメニュー
if (windowVisible)
{
gui.windowBegin(U"NodeEditor", SasaGUI::NoMove | SasaGUI::AutoResize | SasaGUI::NoTitlebar | SasaGUI::AlwaysForeground | SasaGUI::NoMargin, SizeF(), windowPos);
gui.windowSetLayoutType(SasaGUI::LayoutType::Vertical);
if (gui.menuItem(U"IncrementNode"))
{
editor.addNode(std::make_shared<IncrementNode>(), windowPos - Vec2(50, 50));
}
if (gui.menuItem(U"AddNode"))
{
editor.addNode(std::make_shared<AddNode>(), windowPos - Vec2(50, 50));
}
if (gui.menuItem(U"IntegerNode"))
{
editor.addNode(std::make_shared<IntegerNode>(), windowPos - Vec2(50, 50));
}
if (gui.menuItem(U"RealNode"))
{
editor.addNode(std::make_shared<RealNode>(), windowPos - Vec2(50, 50));
}
if (gui.menuItem(U"PreviewNode"))
{
editor.addNode(std::make_shared<PreviewNode>(), windowPos - Vec2(50, 50));
}
if (MouseL.down())
{
windowVisible = false;
}
gui.windowEnd();
}
else if (MouseR.down())
{
windowVisible = true;
windowPos = Cursor::PosF();
}
editor.draw({ 50, 50 });
gui.frameEnd();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment