CustomNodeGraph.pas 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. {
  2. FreePascal / Delphi bindings for ImGui
  3. Copyright (C) 2023 Coldzer0 <Coldzer0 [at] protonmail.ch>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the MIT License.
  6. This program is distributed in the hope that it will be useful,
  7. but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. MIT License for more details.
  10. }
  11. // based on https://gist.github.com/ocornut/7e9b3ec566a333d725d4
  12. Unit CustomNodeGraph;
  13. {$IfDef FPC}
  14. {$mode Delphi}{$H+}
  15. {$ModeSwitch advancedrecords}
  16. {$EndIf}
  17. Interface
  18. Uses
  19. Generics.Collections,
  20. SysUtils,
  21. PasImGui,
  22. PasImGui.Types,
  23. PasImGui.Enums;
  24. Type
  25. { TNode }
  26. TNode = Record
  27. ID: Integer;
  28. Name: Array [0..31] Of AnsiChar;
  29. Pos, Size: ImVec2;
  30. Value: Single;
  31. Color: ImVec4;
  32. InputsCount, OutputsCount: Integer;
  33. Function GetInputSlotPos(slotNo: Integer): ImVec2;
  34. Function GetOutputSlotPos(slotNo: Integer): ImVec2;
  35. Constructor Create(id_: Integer; Const Name_: PAnsiChar;
  36. pos_: ImVec2; Value_: Single; color_: ImVec4; inputsCount_, outputsCount_: Integer);
  37. End;
  38. { TNodeLink }
  39. TNodeLink = Record
  40. InputIdx, InputSlot, OutputIdx, OutputSlot: Integer;
  41. Constructor Create(inputIdx_, inputSlot_, outputIdx_, outputSlot_: Integer);
  42. End;
  43. Procedure ShowExampleAppCustomNodeGraph(opened: PBoolean);
  44. Implementation
  45. Uses
  46. Math;
  47. Var
  48. nodes: TList<TNode>;
  49. links: TList<TNodeLink>;
  50. scrolling: ImVec2 = (x: 0.0; y: 0.0);
  51. node_hovered_in_list: Integer = -1;
  52. node_hovered_in_scene: Integer = -1;
  53. node_selected: Integer = -1;
  54. open_context_menu: Boolean = False;
  55. inited: Boolean = False;
  56. show_grid: Boolean = False;
  57. Procedure ShowExampleAppCustomNodeGraph(opened: PBoolean);
  58. Var
  59. io: PImGuiIO;
  60. node, node_inp, node_out: TNode;
  61. link: TNodeLink;
  62. node_idx, slot_idx, link_idx: Integer;
  63. offset, win_pos, canvas_sz, p1, p2, node_rect_min, node_rect_max, scene_pos: ImVec2;
  64. draw_list: PImDrawList;
  65. GRID_COLOR, node_bg_color: ImU32;
  66. GRID_SZ, x, y: Single;
  67. old_any_active, node_widgets_active, node_moving_active: Boolean;
  68. Const
  69. NODE_SLOT_RADIUS: Single = 4.0;
  70. NODE_WINDOW_PADDING: ImVec2 = (x: 8.0; y: 8.0);
  71. Begin
  72. ImGui.SetNextWindowSize(ImVec2.New(800, 600), ImGuiCond_FirstUseEver);
  73. ImGui.SetNextWindowPosCenter(ImGuiCond_FirstUseEver);
  74. If Not ImGui.Begin_('Custom Node Graph') Then
  75. Begin
  76. ImGui.End_; // Early out if the window is collapsed, as an optimization.
  77. exit;
  78. End;
  79. io := ImGui.GetIO();
  80. If Not inited Then
  81. Begin
  82. nodes := TList<TNode>.Create();
  83. links := TList<TNodeLink>.Create();
  84. nodes.Add(TNode.Create(0, 'MainTex', ImVec2.New(40, 50), 0.5,
  85. ImVec4.New(255, 100, 100), 1, 1));
  86. nodes.Add(TNode.Create(1, 'BumpMap', ImVec2.New(40, 150), 0.42,
  87. ImVec4.New(200, 100, 200), 1, 1));
  88. nodes.Add(TNode.Create(2, 'Combine', ImVec2.New(270, 80), 1.0,
  89. ImVec4.New(0, 200, 100), 2, 2));
  90. links.Add(TNodeLink.Create(0, 0, 2, 0));
  91. links.Add(TNodeLink.Create(1, 0, 2, 1));
  92. inited := True;
  93. End;
  94. // Draw a list of nodes on the left side
  95. ImGui.BeginChild('node_list', ImVec2.New(100, 0));
  96. ImGui.Text('Nodes');
  97. ImGui.Separator();
  98. For node_idx := 0 To Pred(nodes.Count) Do
  99. Begin
  100. node := nodes[node_idx];
  101. ImGui.PushIdInt(node.ID);
  102. If ImGui.Selectable(node.Name, node.ID = node_selected) Then
  103. node_selected := node.ID;
  104. If ImGui.IsItemHovered() Then
  105. Begin
  106. node_hovered_in_list := node.ID;
  107. open_context_menu := Boolean(Ord(open_context_menu) Or
  108. Ord(ImGui.IsMouseClicked(ImGuiMouseButton_Left)));
  109. End;
  110. ImGui.PopID();
  111. End;
  112. ImGui.EndChild();
  113. ImGui.SameLine();
  114. ImGui.BeginGroup();
  115. // Create our child canvas
  116. ImGui.Text('Hold middle mouse button to scroll (%.2f,%.2f)',
  117. [scrolling.x, scrolling.y]);
  118. ImGui.SameLine({ImGui.GetWindowWidth() - 100});
  119. ImGui.Checkbox('Show grid', @show_grid);
  120. ImGui.PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2.New(1, 1));
  121. ImGui.PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2.New(0, 0));
  122. ImGui.PushStyleColor(ImGuiCol_ChildBg, IM_COL32(60, 60, 70, 200));
  123. ImGui.BeginChild('scrolling_region', ImVec2.New(0, 0), ImGuiChildFlags_None,
  124. ImGuiWindowFlags_NoScrollbar Or ImGuiWindowFlags_NoMove);
  125. ImGui.PopStyleVar(); // WindowPadding
  126. ImGui.PushItemWidth(120.0);
  127. offset := ImGui.GetCursorScreenPos() + scrolling;
  128. draw_list := ImGui.GetWindowDrawList();
  129. If show_grid Then
  130. Begin
  131. GRID_COLOR := IM_COL32(200, 200, 200, 40);
  132. GRID_SZ := 64.0;
  133. win_pos := ImGui.GetCursorScreenPos();
  134. canvas_sz := ImGui.GetWindowSize();
  135. x := fmod(scrolling.x, GRID_SZ);
  136. While (x > 0) And (x < canvas_sz.x) Do
  137. Begin
  138. draw_list^.AddLine(ImVec2.New(x, 0.0) + win_pos, ImVec2.New(x, canvas_sz.y) +
  139. win_pos, GRID_COLOR);
  140. x := x + GRID_SZ;
  141. End;
  142. y := fmod(scrolling.y, GRID_SZ);
  143. While (y > 0) And (y < canvas_sz.y) Do
  144. Begin
  145. draw_list^.AddLine(ImVec2.New(0.0, y) + win_pos, ImVec2.New(canvas_sz.x, y) +
  146. win_pos, GRID_COLOR);
  147. y := y + GRID_SZ;
  148. End;
  149. End;
  150. // Display links
  151. draw_list^.ChannelsSplit(2);
  152. draw_list^.ChannelsSetCurrent(0); // Background
  153. For link_idx := 0 To Pred(links.Count) Do
  154. Begin
  155. link := links[link_idx];
  156. node_inp := nodes[link.InputIdx];
  157. node_out := nodes[link.OutputIdx];
  158. p1 := offset + node_inp.GetOutputSlotPos(link.InputSlot);
  159. p2 := offset + node_out.GetInputSlotPos(link.OutputSlot);
  160. draw_list^.AddBezierCubic(p1, p1 + ImVec2.New(+50, 0), p2 +
  161. ImVec2.New(-50, 0), p2, IM_COL32(200, 200, 100, 255), 3.0);
  162. End;
  163. // Display nodes
  164. For node_idx := 0 To Pred(nodes.Count) Do
  165. Begin
  166. node := nodes[node_idx];
  167. ImGui.PushIdInt(node.ID);
  168. node_rect_min := offset + node.Pos;
  169. // Display node contents first
  170. draw_list^.ChannelsSetCurrent(1); // Foreground
  171. old_any_active := ImGui.IsAnyItemActive();
  172. ImGui.SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING);
  173. ImGui.BeginGroup(); // Lock horizontal position
  174. ImGui.Text('%s', [node.Name]);
  175. ImGui.SliderFloat('##value', @node.Value, 0.0, 1.0, 'Alpha %.2f');
  176. ImGui.ColorEdit3('##color', @node.Color.x);
  177. ImGui.EndGroup();
  178. // Save the size of what we have emitted and whether any of the widgets are being used
  179. node_widgets_active := ((Not old_any_active) And ImGui.IsAnyItemActive());
  180. node.Size := ImGui.GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING;
  181. node_rect_max := node_rect_min + node.Size;
  182. // Display node box
  183. draw_list^.ChannelsSetCurrent(0); // Background
  184. ImGui.SetCursorScreenPos(node_rect_min);
  185. ImGui.InvisibleButton('node', node.Size);
  186. If ImGui.IsItemHovered() Then
  187. Begin
  188. node_hovered_in_scene := node.ID;
  189. open_context_menu := open_context_menu Or ImGui.IsMouseClicked(1);
  190. End;
  191. node_moving_active := ImGui.IsItemActive();
  192. If (node_widgets_active Or node_moving_active) Then
  193. node_selected := node.ID;
  194. If (node_moving_active And ImGui.IsMouseDragging(ImGuiMouseButton_Left)) Then
  195. node.Pos := node.Pos + io^.MouseDelta;
  196. If ((node_hovered_in_list = node.ID) Or (node_hovered_in_scene = node.ID) Or
  197. ((node_hovered_in_list = -1) And (node_selected = node.ID))) Then
  198. node_bg_color := IM_COL32(75, 75, 75, 255)
  199. Else
  200. node_bg_color := IM_COL32(60, 60, 60, 255);
  201. draw_list^.AddRectFilled(node_rect_min, node_rect_max, node_bg_color, 4.0);
  202. draw_list^.AddRect(node_rect_min, node_rect_max, IM_COL32(100, 100, 100, 255), 4.0);
  203. For slot_idx := 0 To Pred(node.InputsCount) Do
  204. draw_list^.AddCircleFilled(offset + node.GetInputSlotPos(slot_idx),
  205. NODE_SLOT_RADIUS, IM_COL32(150, 150, 150, 150));
  206. For slot_idx := 0 To Pred(node.OutputsCount) Do
  207. draw_list^.AddCircleFilled(offset + node.GetOutputSlotPos(slot_idx),
  208. NODE_SLOT_RADIUS, IM_COL32(150, 150, 150, 150));
  209. ImGui.PopID();
  210. End;
  211. draw_list^.ChannelsMerge();
  212. // Open context menu
  213. If ImGui.IsMouseReleased(ImGuiMouseButton_Right) Then
  214. Begin
  215. If ImGui.IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) Or
  216. (Not ImGui.IsAnyItemHovered()) Then
  217. Begin
  218. node_hovered_in_list := -1;
  219. node_hovered_in_scene := -1;
  220. ;
  221. node_selected := -1;
  222. open_context_menu := True;
  223. End;
  224. End;
  225. If open_context_menu Then
  226. Begin
  227. ImGui.OpenPopup('context_menu');
  228. If (node_hovered_in_list <> -1) Then
  229. node_selected := node_hovered_in_list;
  230. If (node_hovered_in_scene <> -1) Then
  231. node_selected := node_hovered_in_scene;
  232. End;
  233. // Draw context menu
  234. ImGui.PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2.New(8, 8));
  235. If ImGui.BeginPopup('context_menu') Then
  236. Begin
  237. scene_pos := ImGui.GetMousePosOnOpeningCurrentPopup() - offset;
  238. If node_selected <> -1 Then
  239. Begin
  240. node := nodes[node_selected];
  241. ImGui.Text('Node "%s"', [node.Name]);
  242. ImGui.Separator();
  243. If ImGui.MenuItem('Rename..', nil, False, False) Then
  244. Begin
  245. End;
  246. If ImGui.MenuItem('Delete', nil, False, False) Then
  247. Begin
  248. End;
  249. If ImGui.MenuItem('Copy', nil, False, False) Then
  250. Begin
  251. End;
  252. End
  253. Else
  254. Begin
  255. If ImGui.MenuItem('Add') Then
  256. nodes.Add(Node.Create(nodes.Count, 'New node', scene_pos, 0.5,
  257. ImVec4.New(100, 100, 200), 2, 2));
  258. If ImGui.MenuItem('Past', nil, False, False) Then
  259. Begin
  260. End;
  261. End;
  262. ImGui.EndPopup();
  263. End;
  264. ImGui.PopStyleVar();
  265. // Scrolling
  266. If (ImGui.IsWindowHovered() And (Not ImGui.IsAnyItemActive()) And
  267. ImGui.IsMouseDragging(ImGuiMouseButton_Middle, 0.0)) Then
  268. scrolling := scrolling + io.MouseDelta;
  269. ImGui.PopItemWidth();
  270. ImGui.EndChild();
  271. ImGui.PopStyleColor();
  272. ImGui.PopStyleVar();
  273. ImGui.EndGroup();
  274. ImGui.End_();
  275. End;
  276. { TNode }
  277. Function TNode.GetInputSlotPos(slotNo: Integer): ImVec2;
  278. Begin
  279. Result.x := Pos.x;
  280. Result.y := Pos.y + Size.y * Single(slotNo + 1) / (InputsCount + 1);
  281. End;
  282. Function TNode.GetOutputSlotPos(slotNo: Integer): ImVec2;
  283. Begin
  284. Result.x := Pos.x + Size.x;
  285. Result.y := Pos.y + Size.y * Single(slotNo + 1) / (OutputsCount + 1);
  286. End;
  287. Constructor TNode.Create(id_: Integer; Const Name_: PAnsiChar;
  288. pos_: ImVec2; Value_: Single; color_: ImVec4; inputsCount_, outputsCount_: Integer);
  289. Begin
  290. Self.ID := id_;
  291. StrLCopy(Self.Name, name_, SizeOf(Self.Name) - 1);
  292. Self.Pos := pos_;
  293. Self.Value := value_;
  294. Self.Color := color_;
  295. Self.InputsCount := inputsCount_;
  296. Self.OutputsCount := outputsCount_;
  297. End;
  298. { TNodeLink }
  299. Constructor TNodeLink.Create(inputIdx_, inputSlot_, outputIdx_, outputSlot_: Integer);
  300. Begin
  301. Self.InputIdx := inputIdx_;
  302. Self.InputSlot := inputSlot_;
  303. Self.OutputIdx := outputIdx_;
  304. Self.OutputSlot := outputSlot_;
  305. End;
  306. Initialization
  307. Finalization
  308. nodes.Free;
  309. links.Free;
  310. End.