1 module node_editor;
2 
3 import bindbc.nuklear;
4 
5 import core.stdc.stdio;
6 import core.stdc.stdlib;
7 import core.stdc.string;
8 import core.stdc.math;
9 
10 struct node {
11     int ID;
12     char[32] name;
13     nk_rect bounds;
14     float value;
15     nk_color color;
16     int input_count;
17     int output_count;
18     node *next;
19     node *prev;
20 }
21 
22 struct node_link {
23     int input_id;
24     int input_slot;
25     int output_id;
26     int output_slot;
27     nk_vec2 in_;
28     nk_vec2 out_;
29 }
30 
31 struct node_linking {
32     int active;
33     node *node_;
34     int input_id;
35     int input_slot;
36 }
37 
38 struct node_editor_ {
39     int initialized;
40     node[32] node_buf;
41     node_link[64] links;
42     node *begin;
43     node *end;
44     int node_count;
45     int link_count;
46     nk_rect bounds;
47     node *selected;
48     int show_grid;
49     nk_vec2 scrolling;
50     node_linking linking;
51 }
52 
53 node_editor_ nodeEditor;
54 
55 static void
56 node_editor_push(node_editor_ *editor, node *node)
57 {
58     if (!editor.begin) {
59         node.next = null;
60         node.prev = null;
61         editor.begin = node;
62         editor.end = node;
63     } else {
64         node.prev = editor.end;
65         if (editor.end)
66             editor.end.next = node;
67         node.next = null;
68         editor.end = node;
69     }
70 }
71 
72 static void
73 node_editor_pop(node_editor_ *editor, node *node)
74 {
75     if (node.next)
76         node.next.prev = node.prev;
77     if (node.prev)
78         node.prev.next = node.next;
79     if (editor.end == node)
80         editor.end = node.prev;
81     if (editor.begin == node)
82         editor.begin = node.next;
83     node.next = null;
84     node.prev = null;
85 }
86 
87 static node*
88 node_editor_find(node_editor_ *editor, int ID)
89 {
90     node *iter = editor.begin;
91     while (iter) {
92         if (iter.ID == ID)
93             return iter;
94         iter = iter.next;
95     }
96     return null;
97 }
98 
99 static void
100 node_editor_add(node_editor_ *editor, const(char) *name, nk_rect bounds,
101                 nk_color col, int in_count, int out_count)
102 {
103     static int IDs = 0;
104     node *node;
105     assert(cast(nk_size)editor.node_count < editor.node_buf.length);
106     node = &editor.node_buf[editor.node_count++];
107     node.ID = IDs++;
108     node.value = 0;
109     node.color = nk_rgb(255, 0, 0);
110     node.input_count = in_count;
111     node.output_count = out_count;
112     node.color = col;
113     node.bounds = bounds;
114     strcpy(node.name.ptr, name);
115     node_editor_push(editor, node);
116 }
117 
118 static void
119 node_editor_link(node_editor_ *editor, int in_id, int in_slot,
120                  int out_id, int out_slot)
121 {
122     node_link *link;
123     assert(cast(nk_size)editor.link_count < editor.links.length);
124     link = &editor.links[editor.link_count++];
125     link.input_id = in_id;
126     link.input_slot = in_slot;
127     link.output_id = out_id;
128     link.output_slot = out_slot;
129 }
130 
131 static void
132 node_editor_init(node_editor_ *editor)
133 {
134     memset(editor, 0, (*editor).sizeof);
135     editor.begin = null;
136     editor.end = null;
137     node_editor_add(editor, "Source", nk_rect(40, 10, 180, 220), nk_rgb(255, 0, 0), 0, 1);
138     node_editor_add(editor, "Source", nk_rect(40, 260, 180, 220), nk_rgb(0, 255, 0), 0, 1);
139     node_editor_add(editor, "Combine", nk_rect(400, 100, 180, 220), nk_rgb(0,0,255), 2, 2);
140     node_editor_link(editor, 0, 0, 2, 0);
141     node_editor_link(editor, 1, 0, 2, 1);
142     editor.show_grid = nk_true;
143 }
144 
145 static int
146 node_editor(nk_context *ctx)
147 {
148     int n = 0;
149     nk_rect total_space;
150     const(nk_input) *in_ = &ctx.input;
151     nk_command_buffer *canvas;
152     node *updated = null;
153     node_editor_ *nodedit = &nodeEditor;
154 
155     if (!nodeEditor.initialized) {
156         node_editor_init(&nodeEditor);
157         nodeEditor.initialized = 1;
158     }
159 
160     if (nk_begin(ctx, "NodeEdit", nk_rect(0, 0, 800, 600),
161                  NK_WINDOW_BORDER|NK_WINDOW_NO_SCROLLBAR|NK_WINDOW_MOVABLE|NK_WINDOW_CLOSABLE))
162     {
163         /* allocate complete window space */
164         canvas = nk_window_get_canvas(ctx);
165         total_space = nk_window_get_content_region(ctx);
166         nk_layout_space_begin(ctx, NK_STATIC, total_space.h, nodedit.node_count);
167         {
168             node *it = nodedit.begin;
169             nk_rect size = nk_layout_space_bounds(ctx);
170             nk_panel *node_panel = null;
171 
172             if (nodedit.show_grid) {
173                 /* display grid */
174                 float x, y;
175                 const float grid_size = 32.0f;
176                 const nk_color grid_color = nk_rgb(50, 50, 50);
177                 for (x = cast(float)fmod(size.x - nodedit.scrolling.x, grid_size); x < size.w; x += grid_size)
178                     nk_stroke_line(canvas, x+size.x, size.y, x+size.x, size.y+size.h, 1.0f, grid_color);
179                 for (y = cast(float)fmod(size.y - nodedit.scrolling.y, grid_size); y < size.h; y += grid_size)
180                     nk_stroke_line(canvas, size.x, y+size.y, size.x+size.w, y+size.y, 1.0f, grid_color);
181             }
182 
183             /* execute each node as a movable group */
184             while (it) {
185                 /* calculate scrolled node window position and size */
186                 nk_layout_space_push(ctx, nk_rect(it.bounds.x - nodedit.scrolling.x,
187                                                   it.bounds.y - nodedit.scrolling.y, it.bounds.w, it.bounds.h));
188 
189                 /* execute node window */
190                 if (nk_group_begin(ctx, it.name.ptr, NK_WINDOW_MOVABLE|NK_WINDOW_NO_SCROLLBAR|NK_WINDOW_BORDER|NK_WINDOW_TITLE))
191                 {
192                     /* always have last selected node on top */
193 
194                     node_panel = nk_window_get_panel(ctx);
195                     if (nk_input_mouse_clicked(in_, NK_BUTTON_LEFT, node_panel.bounds) &&
196                         (!(it.prev && nk_input_mouse_clicked(in_, NK_BUTTON_LEFT,
197                                                               nk_layout_space_rect_to_screen(ctx, node_panel.bounds)))) &&
198                         nodedit.end != it)
199                     {
200                         updated = it;
201                     }
202 
203                     /* ================= NODE CONTENT =====================*/
204                     nk_layout_row_dynamic(ctx, 25, 1);
205                     nk_button_color(ctx, it.color);
206                     it.color.r = cast(nk_byte)nk_propertyi(ctx, "#R:", 0, it.color.r, 255, 1,1);
207                     it.color.g = cast(nk_byte)nk_propertyi(ctx, "#G:", 0, it.color.g, 255, 1,1);
208                     it.color.b = cast(nk_byte)nk_propertyi(ctx, "#B:", 0, it.color.b, 255, 1,1);
209                     it.color.a = cast(nk_byte)nk_propertyi(ctx, "#A:", 0, it.color.a, 255, 1,1);
210                     /* ====================================================*/
211                     nk_group_end(ctx);
212                 }
213                 {
214                     /* node connector and linking */
215                     float space;
216                     nk_rect bounds;
217                     bounds = nk_layout_space_rect_to_local(ctx, node_panel.bounds);
218                     bounds.x += nodedit.scrolling.x;
219                     bounds.y += nodedit.scrolling.y;
220                     it.bounds = bounds;
221 
222                     /* output connector */
223                     space = node_panel.bounds.h / cast(float)((it.output_count) + 1);
224                     for (n = 0; n < it.output_count; ++n) {
225                         nk_rect circle;
226                         circle.x = node_panel.bounds.x + node_panel.bounds.w-4;
227                         circle.y = node_panel.bounds.y + space * cast(float)(n+1);
228                         circle.w = 8; circle.h = 8;
229                         nk_fill_circle(canvas, circle, nk_rgb(100, 100, 100));
230 
231                         /* start linking process */
232                         if (nk_input_has_mouse_click_down_in_rect(in_, NK_BUTTON_LEFT, circle, nk_true)) {
233                             nodedit.linking.active = nk_true;
234                             nodedit.linking.node_ = it;
235                             nodedit.linking.input_id = it.ID;
236                             nodedit.linking.input_slot = n;
237                         }
238 
239                         /* draw curve from linked node slot to mouse position */
240                         if (nodedit.linking.active && nodedit.linking.node_ == it &&
241                             nodedit.linking.input_slot == n) {
242                                 nk_vec2 l0 = nk_vec2(circle.x + 3, circle.y + 3);
243                                 nk_vec2 l1 = in_.mouse.pos;
244                                 nk_stroke_curve(canvas, l0.x, l0.y, l0.x + 50.0f, l0.y,
245                                                 l1.x - 50.0f, l1.y, l1.x, l1.y, 1.0f, nk_rgb(100, 100, 100));
246                             }
247                     }
248 
249                     /* input connector */
250                     space = node_panel.bounds.h / cast(float)((it.input_count) + 1);
251                     for (n = 0; n < it.input_count; ++n) {
252                         nk_rect circle;
253                         circle.x = node_panel.bounds.x-4;
254                         circle.y = node_panel.bounds.y + space * cast(float)(n+1);
255                         circle.w = 8; circle.h = 8;
256                         nk_fill_circle(canvas, circle, nk_rgb(100, 100, 100));
257                         if (nk_input_is_mouse_released(in_, NK_BUTTON_LEFT) &&
258                             nk_input_is_mouse_hovering_rect(in_, circle) &&
259                             nodedit.linking.active && nodedit.linking.node_ != it) {
260                                 nodedit.linking.active = nk_false;
261                                 node_editor_link(nodedit, nodedit.linking.input_id,
262                                                  nodedit.linking.input_slot, it.ID, n);
263                             }
264                     }
265                 }
266                 it = it.next;
267             }
268 
269             /* reset linking connection */
270             if (nodedit.linking.active && nk_input_is_mouse_released(in_, NK_BUTTON_LEFT)) {
271                 nodedit.linking.active = nk_false;
272                 nodedit.linking.node_ = null;
273                 fprintf(stdout, "linking failed\n");
274             }
275 
276             /* draw each link */
277             for (n = 0; n < nodedit.link_count; ++n) {
278                 node_link *link = &nodedit.links[n];
279                 node *ni = node_editor_find(nodedit, link.input_id);
280                 node *no = node_editor_find(nodedit, link.output_id);
281                 float spacei = node_panel.bounds.h / cast(float)((ni.output_count) + 1);
282                 float spaceo = node_panel.bounds.h / cast(float)((no.input_count) + 1);
283                 nk_vec2 l0 = nk_layout_space_to_screen(ctx,
284                                                               nk_vec2(ni.bounds.x + ni.bounds.w, 3.0f + ni.bounds.y + spacei * cast(float)(link.input_slot+1)));
285                 nk_vec2 l1 = nk_layout_space_to_screen(ctx,
286                                                               nk_vec2(no.bounds.x, 3.0f + no.bounds.y + spaceo * cast(float)(link.output_slot+1)));
287 
288                 l0.x -= nodedit.scrolling.x;
289                 l0.y -= nodedit.scrolling.y;
290                 l1.x -= nodedit.scrolling.x;
291                 l1.y -= nodedit.scrolling.y;
292                 nk_stroke_curve(canvas, l0.x, l0.y, l0.x + 50.0f, l0.y,
293                                 l1.x - 50.0f, l1.y, l1.x, l1.y, 1.0f, nk_rgb(100, 100, 100));
294             }
295 
296             if (updated) {
297                 /* reshuffle nodes to have least recently selected node on top */
298                 node_editor_pop(nodedit, updated);
299                 node_editor_push(nodedit, updated);
300             }
301 
302             /* node selection */
303             if (nk_input_mouse_clicked(in_, NK_BUTTON_LEFT, nk_layout_space_bounds(ctx))) {
304                 it = nodedit.begin;
305                 nodedit.selected = null;
306                 nodedit.bounds = nk_rect(in_.mouse.pos.x, in_.mouse.pos.y, 100, 200);
307                 while (it) {
308                     nk_rect b = nk_layout_space_rect_to_screen(ctx, it.bounds);
309                     b.x -= nodedit.scrolling.x;
310                     b.y -= nodedit.scrolling.y;
311                     if (nk_input_is_mouse_hovering_rect(in_, b))
312                         nodedit.selected = it;
313                     it = it.next;
314                 }
315             }
316 
317             /* contextual menu */
318             if (nk_contextual_begin(ctx, 0, nk_vec2(100, 220), nk_window_get_bounds(ctx))) {
319                 const(char) *[2]grid_option = ["Show Grid", "Hide Grid"];
320                 nk_layout_row_dynamic(ctx, 25, 1);
321                 if (nk_contextual_item_label(ctx, "New", NK_TEXT_CENTERED))
322                     node_editor_add(nodedit, "New", nk_rect(400, 260, 180, 220),
323                                     nk_rgb(255, 255, 255), 1, 2);
324                 if (nk_contextual_item_label(ctx, grid_option[nodedit.show_grid],NK_TEXT_CENTERED))
325                     nodedit.show_grid = !nodedit.show_grid;
326                 nk_contextual_end(ctx);
327             }
328         }
329         nk_layout_space_end(ctx);
330 
331         /* window content scrolling */
332         if (nk_input_is_mouse_hovering_rect(in_, nk_window_get_bounds(ctx)) &&
333             nk_input_is_mouse_down(in_, NK_BUTTON_MIDDLE)) {
334                 nodedit.scrolling.x += in_.mouse.delta.x;
335                 nodedit.scrolling.y += in_.mouse.delta.y;
336             }
337     }
338     nk_end(ctx);
339     return !nk_window_is_closed(ctx, "NodeEdit");
340 }