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 }