1#![allow(clippy::all)]
2#![allow(noop_method_call)]
3use super::traits::Renderable;
4
5#[derive(Clone, Debug, PartialEq, Copy)]
6pub enum PinType {
7 Flow,
8 Bool,
9 Int,
10 Float,
11 Vec2,
12 Vec3,
13 Vec4,
14 Color,
15 Texture,
16 Object,
17 Any,
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct NodePin {
22 pub id: String,
23 pub name: String,
24 pub pin_type: PinType,
25 pub is_input: bool,
26 pub connected_to: Option<String>,
27 pub default_value: String,
28}
29
30impl NodePin {
31 #[inline]
32 pub fn input(id: String, name: String, pin_type: PinType) -> NodePin {
33 NodePin {
34 id,
35 name,
36 pin_type,
37 is_input: true,
38 connected_to: None,
39 default_value: "".to_string(),
40 }
41 }
42 #[inline]
43 pub fn output(id: String, name: String, pin_type: PinType) -> NodePin {
44 NodePin {
45 id,
46 name,
47 pin_type,
48 is_input: false,
49 connected_to: None,
50 default_value: "".to_string(),
51 }
52 }
53 #[inline]
54 pub fn default_value(mut self, value: String) -> NodePin {
55 self.default_value = value;
56 self
57 }
58 #[inline]
59 pub fn connect(mut self, target: String) -> NodePin {
60 self.connected_to = Some(target.to_string());
61 self
62 }
63 #[inline]
64 pub fn get_color(&self) -> String {
65 match self.pin_type {
66 PinType::Flow => "#ffffff".to_string(),
67 PinType::Bool => "#e94560".to_string(),
68 PinType::Int => "#00d9ff".to_string(),
69 PinType::Float => "#4ade80".to_string(),
70 PinType::Vec2 => "#facc15".to_string(),
71 PinType::Vec3 => "#f59e0b".to_string(),
72 PinType::Vec4 => "#a855f7".to_string(),
73 PinType::Color => "#ec4899".to_string(),
74 PinType::Texture => "#fb923c".to_string(),
75 PinType::Object => "#3b82f6".to_string(),
76 PinType::Any => "#888888".to_string(),
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Copy)]
82pub enum NodeCategory {
83 Math,
84 Logic,
85 Texture,
86 Color,
87 Vector,
88 Flow,
89 Event,
90 Variable,
91 Custom,
92}
93
94#[derive(Debug, Clone)]
95pub struct GraphNode {
96 pub id: String,
97 pub title: String,
98 pub category: NodeCategory,
99 pub x: f32,
100 pub y: f32,
101 pub inputs: Vec<NodePin>,
102 pub outputs: Vec<NodePin>,
103 pub collapsed: bool,
104 pub preview_enabled: bool,
105}
106
107impl GraphNode {
108 #[inline]
109 pub fn new(id: String, title: String, category: NodeCategory) -> GraphNode {
110 GraphNode {
111 id,
112 title,
113 category,
114 x: 0.0,
115 y: 0.0,
116 inputs: Vec::new(),
117 outputs: Vec::new(),
118 collapsed: false,
119 preview_enabled: false,
120 }
121 }
122 #[inline]
123 pub fn position(mut self, x: f32, y: f32) -> GraphNode {
124 self.x = x;
125 self.y = y;
126 self
127 }
128 #[inline]
129 pub fn input(mut self, pin: NodePin) -> GraphNode {
130 self.inputs.push(pin);
131 self
132 }
133 #[inline]
134 pub fn output(mut self, pin: NodePin) -> GraphNode {
135 self.outputs.push(pin);
136 self
137 }
138 #[inline]
139 pub fn collapsed(mut self, collapsed: bool) -> GraphNode {
140 self.collapsed = collapsed;
141 self
142 }
143 #[inline]
144 pub fn preview(mut self, enabled: bool) -> GraphNode {
145 self.preview_enabled = enabled;
146 self
147 }
148 #[inline]
149 pub fn get_category_color(&self) -> String {
150 match self.category {
151 NodeCategory::Math => "#4ade80".to_string(),
152 NodeCategory::Logic => "#e94560".to_string(),
153 NodeCategory::Texture => "#fb923c".to_string(),
154 NodeCategory::Color => "#ec4899".to_string(),
155 NodeCategory::Vector => "#facc15".to_string(),
156 NodeCategory::Flow => "#ffffff".to_string(),
157 NodeCategory::Event => "#3b82f6".to_string(),
158 NodeCategory::Variable => "#a855f7".to_string(),
159 NodeCategory::Custom => "#888888".to_string(),
160 }
161 }
162}
163
164impl Renderable for GraphNode {
165 #[inline]
166 fn render(self) -> String {
167 let header_color = self.get_category_color();
168 let mut inputs_html = "".to_string();
169 for pin in &self.inputs {
170 let color = pin.get_color();
171 let connected_class = match pin.connected_to.clone() {
172 Some(_) => "connected".to_string(),
173 None => "".to_string(),
174 };
175 inputs_html += format!(
176 "
177 <div class='node-pin input {}'>
178 <div class='pin-socket' style='background: {};' data-pin='{}'></div>
179 <span class='pin-name'>{}</span>
180 </div>
181 ",
182 connected_class,
183 color,
184 pin.id,
185 pin.name.clone()
186 )
187 .as_str();
188 }
189 let mut outputs_html = "".to_string();
190 for pin in &self.outputs {
191 let color = pin.get_color();
192 let connected_class = match pin.connected_to.clone() {
193 Some(_) => "connected".to_string(),
194 None => "".to_string(),
195 };
196 outputs_html += format!(
197 "
198 <div class='node-pin output {}'>
199 <span class='pin-name'>{}</span>
200 <div class='pin-socket' style='background: {};' data-pin='{}'></div>
201 </div>
202 ",
203 connected_class,
204 pin.name.clone(),
205 color,
206 pin.id
207 )
208 .as_str();
209 }
210 let preview_html = {
211 if self.preview_enabled {
212 "<div class='node-preview'><canvas class='preview-canvas'></canvas></div>"
213 .to_string()
214 } else {
215 "".to_string()
216 }
217 };
218 format!(
219 "
220 <div class='graph-node' id='{}' style='left: {}px; top: {}px;'>
221 <div class='node-header' style='background: {};'>
222 <span class='node-title'>{}</span>
223 <div class='node-actions'>
224 <button class='node-btn preview' title='Preview'>👁</button>
225 <button class='node-btn collapse' title='Collapse'>−</button>
226 </div>
227 </div>
228 <div class='node-body'>
229 <div class='node-inputs'>
230 {}
231 </div>
232 <div class='node-outputs'>
233 {}
234 </div>
235 </div>
236 {}
237 </div>
238 ",
239 self.id,
240 self.x,
241 self.y,
242 header_color,
243 self.title,
244 inputs_html,
245 outputs_html,
246 preview_html
247 )
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
252pub struct NodeConnection {
253 pub from_node: String,
254 pub from_pin: String,
255 pub to_node: String,
256 pub to_pin: String,
257}
258
259#[derive(Debug, Clone)]
260pub struct NodeGraph {
261 pub width: i32,
262 pub height: i32,
263 pub nodes: Vec<GraphNode>,
264 pub connections: Vec<NodeConnection>,
265 pub zoom: f32,
266 pub pan_x: f32,
267 pub pan_y: f32,
268 pub show_grid: bool,
269 pub on_change: String,
270}
271
272impl NodeGraph {
273 #[inline]
274 pub fn new() -> NodeGraph {
275 NodeGraph {
276 width: 800,
277 height: 600,
278 nodes: Vec::new(),
279 connections: Vec::new(),
280 zoom: 1.0,
281 pan_x: 0.0,
282 pan_y: 0.0,
283 show_grid: true,
284 on_change: "".to_string(),
285 }
286 }
287 #[inline]
288 pub fn size(mut self, width: i32, height: i32) -> NodeGraph {
289 self.width = width;
290 self.height = height;
291 self
292 }
293 #[inline]
294 pub fn node(mut self, node: GraphNode) -> NodeGraph {
295 self.nodes.push(node);
296 self
297 }
298 #[inline]
299 pub fn connect(
300 mut self,
301 from_node: String,
302 from_pin: String,
303 to_node: String,
304 to_pin: String,
305 ) -> NodeGraph {
306 self.connections.push(NodeConnection {
307 from_node,
308 from_pin,
309 to_node,
310 to_pin,
311 });
312 self
313 }
314 #[inline]
315 pub fn zoom(mut self, zoom: f32) -> NodeGraph {
316 self.zoom = zoom;
317 self
318 }
319 #[inline]
320 pub fn pan(mut self, x: f32, y: f32) -> NodeGraph {
321 self.pan_x = x;
322 self.pan_y = y;
323 self
324 }
325}
326
327impl Renderable for NodeGraph {
328 #[inline]
329 fn render(self) -> String {
330 let mut nodes_html = "".to_string();
331 for n in &self.nodes {
332 nodes_html = format!(
333 "{}{}{}",
334 nodes_html,
335 n.clone().render().as_str(),
336 "
337"
338 );
339 }
340 let mut connections_html = "".to_string();
341 for c in &self.connections {
342 connections_html += format!(
343 "
344 <path class='node-connection'
345 data-from='{}:{}'
346 data-to='{}:{}'/>
347 ",
348 c.from_node.clone(),
349 c.from_pin.clone(),
350 c.to_node.clone(),
351 c.to_pin.clone()
352 )
353 .as_str();
354 }
355 let grid_class = {
356 if self.show_grid {
357 "show-grid".to_string()
358 } else {
359 "".to_string()
360 }
361 };
362 format!(
363 "
364 <div class='node-graph {}' style='width: {}px; height: {}px;'>
365 <div class='graph-toolbar'>
366 <button onclick='addNode()'>+ Add Node</button>
367 <span class='toolbar-sep'></span>
368 <button onclick='zoomIn()'>🔍+</button>
369 <button onclick='zoomOut()'>🔍−</button>
370 <button onclick='fitAll()'>⊞</button>
371 <span class='zoom-level'>{:.0}%</span>
372 </div>
373 <div class='graph-canvas'
374 style='transform: scale({}) translate({}px, {}px);'>
375 <svg class='connections-layer'>
376 {}
377 </svg>
378 <div class='nodes-layer'>
379 {}
380 </div>
381 </div>
382 <div class='graph-minimap'>
383 <div class='minimap-viewport'></div>
384 </div>
385 </div>
386 ",
387 grid_class,
388 self.width,
389 self.height,
390 self.zoom * 100.0,
391 self.zoom,
392 self.pan_x,
393 self.pan_y,
394 connections_html,
395 nodes_html
396 )
397 }
398}
399
400#[inline]
401pub fn node_graph_styles() -> String {
402 "
403 .node-graph {
404 position: relative;
405 background: #0a0a1a;
406 border-radius: 8px;
407 overflow: hidden;
408 }
409
410 .node-graph.show-grid {
411 background-image:
412 linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
413 linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
414 background-size: 20px 20px;
415 }
416
417 .graph-toolbar {
418 position: absolute;
419 top: 8px;
420 left: 8px;
421 display: flex;
422 gap: 4px;
423 padding: 4px;
424 background: rgba(22, 33, 62, 0.9);
425 border-radius: 4px;
426 z-index: 100;
427 }
428
429 .graph-toolbar button {
430 padding: 4px 8px;
431 border: none;
432 border-radius: 4px;
433 background: #0f3460;
434 color: #888;
435 cursor: pointer;
436 }
437
438 .graph-toolbar button:hover {
439 background: #1a4a8a;
440 color: #e0e0e0;
441 }
442
443 .toolbar-sep {
444 width: 1px;
445 background: #333;
446 }
447
448 .zoom-level {
449 padding: 0 8px;
450 font-size: 12px;
451 color: #666;
452 }
453
454 .graph-canvas {
455 position: absolute;
456 top: 0;
457 left: 0;
458 width: 100%;
459 height: 100%;
460 transform-origin: center center;
461 }
462
463 .connections-layer {
464 position: absolute;
465 top: 0;
466 left: 0;
467 width: 100%;
468 height: 100%;
469 pointer-events: none;
470 }
471
472 .node-connection {
473 fill: none;
474 stroke: #666;
475 stroke-width: 2;
476 }
477
478 .nodes-layer {
479 position: absolute;
480 top: 0;
481 left: 0;
482 }
483
484 /* Graph Node */
485 .graph-node {
486 position: absolute;
487 min-width: 180px;
488 background: #16213e;
489 border-radius: 8px;
490 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
491 user-select: none;
492 }
493
494 .node-header {
495 display: flex;
496 align-items: center;
497 justify-content: space-between;
498 padding: 8px 12px;
499 border-radius: 8px 8px 0 0;
500 cursor: move;
501 }
502
503 .node-title {
504 font-size: 12px;
505 font-weight: 600;
506 color: #1a1a2e;
507 }
508
509 .node-actions {
510 display: flex;
511 gap: 4px;
512 }
513
514 .node-btn {
515 width: 20px;
516 height: 20px;
517 border: none;
518 background: rgba(0,0,0,0.2);
519 border-radius: 4px;
520 font-size: 10px;
521 cursor: pointer;
522 color: rgba(0,0,0,0.6);
523 }
524
525 .node-btn:hover {
526 background: rgba(0,0,0,0.4);
527 color: rgba(0,0,0,0.8);
528 }
529
530 .node-body {
531 display: flex;
532 justify-content: space-between;
533 padding: 8px 0;
534 }
535
536 .node-inputs, .node-outputs {
537 display: flex;
538 flex-direction: column;
539 gap: 4px;
540 }
541
542 .node-pin {
543 display: flex;
544 align-items: center;
545 gap: 8px;
546 padding: 4px 12px;
547 cursor: pointer;
548 }
549
550 .node-pin.input {
551 flex-direction: row;
552 }
553
554 .node-pin.output {
555 flex-direction: row-reverse;
556 }
557
558 .pin-socket {
559 width: 12px;
560 height: 12px;
561 border-radius: 50%;
562 border: 2px solid rgba(255,255,255,0.3);
563 transition: transform 0.15s;
564 }
565
566 .node-pin:hover .pin-socket {
567 transform: scale(1.3);
568 border-color: white;
569 }
570
571 .node-pin.connected .pin-socket {
572 border-color: white;
573 }
574
575 .pin-name {
576 font-size: 11px;
577 color: #888;
578 }
579
580 .node-preview {
581 padding: 8px;
582 border-top: 1px solid rgba(255,255,255,0.1);
583 }
584
585 .preview-canvas {
586 width: 100%;
587 height: 60px;
588 background: #0a0a1a;
589 border-radius: 4px;
590 }
591
592 /* Minimap */
593 .graph-minimap {
594 position: absolute;
595 bottom: 8px;
596 right: 8px;
597 width: 150px;
598 height: 100px;
599 background: rgba(22, 33, 62, 0.9);
600 border-radius: 4px;
601 border: 1px solid #333;
602 }
603
604 .minimap-viewport {
605 position: absolute;
606 border: 2px solid #e94560;
607 background: rgba(233, 69, 96, 0.1);
608 }
609 "
610 .to_string()
611}