CustomNodeGraph.pas 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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. //{$CODEALIGN LOCALMIN=16}
  17. {$inline on }
  18. {$EndIf}
  19. Interface
  20. Uses
  21. Generics.Collections,
  22. SysUtils,
  23. PasImGui;
  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; pos_: ImVec2; Value_: Single;
  36. 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. inited: Boolean = False;
  52. show_grid: Boolean = True;
  53. node_selected: Integer = -1;
  54. Procedure ShowExampleAppCustomNodeGraph(opened: PBoolean);
  55. Var
  56. io: PImGuiIO;
  57. node, node_inp, node_out: TNode;
  58. link: TNodeLink;
  59. node_idx, slot_idx, link_idx: Integer;
  60. offset, win_pos, canvas_sz, p1, p2, node_rect_min, node_rect_max, scene_pos: ImVec2;
  61. draw_list: PImDrawList;
  62. GRID_COLOR, node_bg_color: ImU32;
  63. x, y: Single;
  64. old_any_active, node_widgets_active, node_moving_active: Boolean;
  65. open_context_menu: Boolean;
  66. node_hovered_in_list: Integer;
  67. node_hovered_in_scene: Integer;
  68. Const
  69. GRID_SZ : Single = 64.0;
  70. NODE_SLOT_RADIUS: Single = 4.0;
  71. NODE_WINDOW_PADDING: ImVec2 = (x: 8.0; y: 8.0);
  72. Begin
  73. ImGui.SetNextWindowSize(ImVec2.New(800, 600), ImGuiCond_FirstUseEver);
  74. //ImGui.SetNextWindowPosCenter(ImGuiCond_FirstUseEver);
  75. If Not ImGui.Begin_('Example: Custom Node Graph', opened) Then
  76. Begin
  77. ImGui.End_; // Early out if the window is collapsed, as an optimization.
  78. exit;
  79. End;
  80. io := ImGui.GetIO();
  81. If Not inited Then
  82. Begin
  83. nodes := TList<TNode>.Create();
  84. links := TList<TNodeLink>.Create();
  85. nodes.Add(TNode.Create(0, 'MainTex', ImVec2.New(40, 50), 0.5, ImVec4.New(255, 100, 100), 1, 1));
  86. nodes.Add(TNode.Create(1, 'BumpMap', ImVec2.New(40, 160), 0.42, ImVec4.New(200, 100, 200), 1, 1));
  87. nodes.Add(TNode.Create(2, 'Combine', ImVec2.New(270, 80), 0.0, ImVec4.New(0, 200, 100), 2, 2));
  88. links.Add(TNodeLink.Create(0, 0, 2, 0));
  89. links.Add(TNodeLink.Create(1, 0, 2, 1));
  90. inited := True;
  91. End;
  92. open_context_menu := False;
  93. node_hovered_in_list := -1;
  94. node_hovered_in_scene := -1;
  95. // Draw a list of nodes on the left side
  96. ImGui.BeginChild('node_list', ImVec2.New(150, 0));
  97. ImGui.Text('Nodes');
  98. ImGui.Separator();
  99. For node_idx := 0 To Pred(nodes.Count) Do
  100. Begin
  101. node := nodes[node_idx];
  102. ImGui.PushId(node.ID);
  103. If ImGui.Selectable(node.Name, node.ID = node_selected) Then
  104. node_selected := node.ID;
  105. If ImGui.IsItemHovered() Then
  106. Begin
  107. node_hovered_in_list := node.ID;
  108. open_context_menu := open_context_menu Or ImGui.IsMouseClicked(ImGuiMouseButton_Right);
  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)', [scrolling.x, scrolling.y]);
  117. ImGui.SameLine();
  118. ImGui.Checkbox('Show grid', @show_grid);
  119. ImGui.PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2.New(1, 1));
  120. ImGui.PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2.New(0, 0));
  121. ImGui.PushStyleColor(ImGuiCol_ChildBg, IM_COL32(60, 60, 70, 200));
  122. ImGui.BeginChild('scrolling_region', ImVec2.New(0, 0), ImGuiChildFlags_Border,
  123. ImGuiWindowFlags_NoScrollbar Or ImGuiWindowFlags_NoMove);
  124. ImGui.PopStyleVar(); // WindowPadding
  125. ImGui.PushItemWidth(120.0);
  126. offset := ImGui.GetCursorScreenPos() + scrolling;
  127. draw_list := ImGui.GetWindowDrawList();
  128. // Display grid
  129. If show_grid Then
  130. Begin
  131. GRID_COLOR := IM_COL32(200, 200, 200, 40);
  132. win_pos := ImGui.GetCursorScreenPos();
  133. canvas_sz := ImGui.GetWindowSize();
  134. x := Math.FMod(scrolling.x, GRID_SZ);
  135. While x < canvas_sz.x Do
  136. Begin
  137. draw_list^.AddLine(ImVec2.New(x, 0.0) + win_pos, ImVec2.New(x, canvas_sz.y) + win_pos, GRID_COLOR);
  138. x += GRID_SZ;
  139. End;
  140. //y := scrolling.y - GRID_SZ * Int(scrolling.y/GRID_SZ);
  141. y := FMod(scrolling.y, GRID_SZ);
  142. While y < canvas_sz.y Do
  143. Begin
  144. draw_list^.AddLine(ImVec2.New(0.0, y) + win_pos, ImVec2.New(canvas_sz.x, y) + win_pos, GRID_COLOR);
  145. y += GRID_SZ;
  146. End;
  147. End;
  148. // Display links
  149. draw_list^.ChannelsSplit(2);
  150. draw_list^.ChannelsSetCurrent(0); // Background
  151. For link_idx := 0 To Pred(links.Count) Do
  152. Begin
  153. link := links[link_idx];
  154. node_inp := nodes[link.InputIdx];
  155. node_out := nodes[link.OutputIdx];
  156. node_out.Value += node_inp.Value;
  157. p1 := offset + node_inp.GetOutputSlotPos(link.InputSlot);
  158. p2 := offset + node_out.GetInputSlotPos(link.OutputSlot);
  159. draw_list^.AddBezierCubic(p1, p1 + ImVec2.New(+50, 0), p2 + ImVec2.New(-50, 0), p2, IM_COL32(200, 200, 100, 255), 3.0);
  160. End;
  161. // Display nodes
  162. For node_idx := 0 To Pred(nodes.Count) Do
  163. Begin
  164. node := nodes[node_idx];
  165. ImGui.PushId(node.ID);
  166. node_rect_min := offset + node.Pos;
  167. // Display node contents first
  168. draw_list^.ChannelsSetCurrent(1); // Foreground
  169. old_any_active := ImGui.IsAnyItemActive();
  170. ImGui.SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING);
  171. ImGui.BeginGroup(); // Lock horizontal position
  172. ImGui.Text('%s', [node.Name]);
  173. ImGui.SliderFloat('##value', @node.Value, 0.0, 1.0, 'Alpha %.2f');
  174. ImGui.ColorEdit3('##color', @node.Color.x);
  175. ImGui.EndGroup();
  176. // Save the size of what we have emitted and whether any of the widgets are being used
  177. node_widgets_active := ((Not old_any_active) And ImGui.IsAnyItemActive());
  178. node.Size := ImGui.GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING;
  179. node_rect_max := node_rect_min + node.Size;
  180. // Display node box
  181. draw_list^.ChannelsSetCurrent(0); // Background
  182. ImGui.SetCursorScreenPos(node_rect_min);
  183. ImGui.InvisibleButton('node', node.Size);
  184. If ImGui.IsItemHovered() Then
  185. Begin
  186. node_hovered_in_scene := node.ID;
  187. open_context_menu := open_context_menu Or ImGui.IsMouseClicked(1);
  188. End;
  189. node_moving_active := ImGui.IsItemActive();
  190. If (node_widgets_active Or node_moving_active) Then
  191. node_selected := node.ID;
  192. If (node_moving_active And ImGui.IsMouseDragging(ImGuiMouseButton_Left)) Then
  193. node.Pos += io^.MouseDelta;
  194. If ((node_hovered_in_list = node.ID) Or (node_hovered_in_scene = node.ID) Or
  195. ((node_hovered_in_list = -1) And (node_selected = node.ID))) Then
  196. node_bg_color := IM_COL32(75, 75, 75, 255)
  197. Else
  198. node_bg_color := IM_COL32(60, 60, 60, 255);
  199. draw_list^.AddRectFilled(node_rect_min, node_rect_max, node_bg_color, 4.0);
  200. draw_list^.AddRect(node_rect_min, node_rect_max, IM_COL32(100, 100, 100, 255), 4.0);
  201. For slot_idx := 0 To Pred(node.InputsCount) Do
  202. draw_list^.AddCircleFilled(offset + node.GetInputSlotPos(slot_idx),
  203. NODE_SLOT_RADIUS, IM_COL32(150, 150, 150, 150));
  204. For slot_idx := 0 To Pred(node.OutputsCount) Do
  205. draw_list^.AddCircleFilled(offset + node.GetOutputSlotPos(slot_idx),
  206. NODE_SLOT_RADIUS, IM_COL32(150, 150, 150, 150));
  207. ImGui.PopID();
  208. nodes[node_idx] := node;
  209. End;
  210. draw_list^.ChannelsMerge();
  211. // Open context menu
  212. If ImGui.IsMouseReleased(ImGuiMouseButton_Right) Then
  213. Begin
  214. If ImGui.IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) Or (Not ImGui.IsAnyItemHovered()) Then
  215. Begin
  216. node_selected := -1;
  217. node_hovered_in_list := -1;
  218. node_hovered_in_scene := -1;
  219. open_context_menu := True;
  220. End;
  221. End;
  222. If open_context_menu Then
  223. Begin
  224. ImGui.OpenPopup('context_menu');
  225. If (node_hovered_in_list <> -1) Then
  226. node_selected := node_hovered_in_list;
  227. If (node_hovered_in_scene <> -1) Then
  228. node_selected := node_hovered_in_scene;
  229. End;
  230. // Draw context menu
  231. ImGui.PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2.New(8, 8));
  232. If ImGui.BeginPopup('context_menu') Then
  233. Begin
  234. scene_pos := ImGui.GetMousePosOnOpeningCurrentPopup() - offset;
  235. If node_selected <> -1 Then
  236. Begin
  237. node := nodes[node_selected];
  238. ImGui.Text('Node "%s"', [node.Name]);
  239. ImGui.Separator();
  240. If ImGui.MenuItem('Rename..', nil, False, False) Then
  241. Begin
  242. End;
  243. If ImGui.MenuItem('Delete', nil, False, False) Then
  244. Begin
  245. End;
  246. If ImGui.MenuItem('Copy', nil, False, False) Then
  247. Begin
  248. End;
  249. End
  250. Else
  251. Begin
  252. If ImGui.MenuItem('Add') Then
  253. nodes.Add(Node.Create(nodes.Count, 'New node', scene_pos, 0.5, ImVec4.New(100, 100, 200), 2, 2));
  254. If ImGui.MenuItem('Past', nil, False, False) Then
  255. Begin
  256. End;
  257. End;
  258. ImGui.EndPopup();
  259. End;
  260. ImGui.PopStyleVar();
  261. // Scrolling
  262. If (ImGui.IsWindowHovered() And (Not ImGui.IsAnyItemActive()) And ImGui.IsMouseDragging(
  263. ImGuiMouseButton_Middle, 0.0)) Then
  264. scrolling += io.MouseDelta;
  265. ImGui.PopItemWidth();
  266. ImGui.EndChild();
  267. ImGui.PopStyleColor();
  268. ImGui.PopStyleVar();
  269. ImGui.EndGroup();
  270. ImGui.End_();
  271. End;
  272. { TNode }
  273. Function TNode.GetInputSlotPos(slotNo: Integer): ImVec2;
  274. Begin
  275. Result.x := Pos.x;
  276. Result.y := Pos.y + Size.y * Single(slotNo + 1) / Single(InputsCount + 1);
  277. End;
  278. Function TNode.GetOutputSlotPos(slotNo: Integer): ImVec2;
  279. Begin
  280. Result.x := Pos.x + Size.x;
  281. Result.y := Pos.y + Size.y * Single(slotNo + 1) / Single(OutputsCount + 1);
  282. End;
  283. Constructor TNode.Create(id_: Integer; Const Name_: PAnsiChar; pos_: ImVec2; Value_: Single;
  284. color_: ImVec4; inputsCount_, outputsCount_: Integer);
  285. Begin
  286. Self.ID := id_;
  287. StrLCopy(Self.Name, name_, SizeOf(Self.Name) - 1);
  288. Self.Pos := pos_;
  289. Self.Value := value_;
  290. Self.Color := color_;
  291. Self.InputsCount := inputsCount_;
  292. Self.OutputsCount := outputsCount_;
  293. End;
  294. { TNodeLink }
  295. Constructor TNodeLink.Create(inputIdx_, inputSlot_, outputIdx_, outputSlot_: Integer);
  296. Begin
  297. Self.InputIdx := inputIdx_;
  298. Self.InputSlot := inputSlot_;
  299. Self.OutputIdx := outputIdx_;
  300. Self.OutputSlot := outputSlot_;
  301. End;
  302. Initialization
  303. Finalization
  304. nodes.Free;
  305. links.Free;
  306. End.