Created
August 2, 2020 07:34
-
-
Save sthairno/98b76ab35f95d3792c172374de20d6b9 to your computer and use it in GitHub Desktop.
S3D_NodeEditor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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