• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 29954885845

22 Jul 2026 08:22PM UTC coverage: 63.479% (+0.01%) from 63.465%
29954885845

push

github

tyevco
fix: Resolve CodeQL alerts in src tier

Fixes 46 of 183 CodeQL alerts across src/ runtime libraries; the
remaining 137 are deliberately dismissed with per-alert rationale in
dismissals.tsv (false positive / won't fix / used in tests).

- catch-of-all-exceptions: type SaveManager and EncryptedPersistenceApi
  save-slot probe catches (IOException, UnauthorizedAccessException,
  InvalidDataException, EndOfStreamException, JsonException); narrow
  TestBridgeClient message parsing to JsonException, EntitySnapshot
  conversion to InvalidCastException/FormatException/OverflowException,
  UdpTransport disconnect send to SocketException/ObjectDisposedException,
  and UIColorPicker hex parsing to FormatException/OverflowException/
  ArgumentException. Bridge/IPC dispatch, logging, hot-reload and
  background-worker containment boundaries are kept and documented.
- loss-of-precision: float-typed arithmetic in WavDecoder,
  SilkAudioContext, AudioClipAsset, GridCoordinate, SilkTextRenderer,
  UdpTransport, ParallelProfiler; FleeAction's intentional integer
  centering made explicit via an int local.
- local-shadows-member: rename shadowing locals in GraphRenderSystem;
  rename Silk2DRenderer's projection field to screenProjection and
  EnsureCapacity parameters (interface parameter names kept for S927).
- constant-condition: drop redundant checksum conjunct in Ghost/Replay
  file formats, hoist device null check out of PauseAll loop, and
  replace the always-false ownsEntity/existingTypes conjunct in
  NetworkServerPlugin owner-state validation.
- useless-upcast/cast-to-self: assign World directly to capability
  fields in StateControllerImpl; drop redundant collection-expression
  casts in AssetManifest/AssetEntry.
- complex-condition: pattern-match Unicode range checks in text shapers.
- inefficient-containskey: TryGetValue in LogAssertions.
- equality-on-floats, obsolete prefab mock calls, ref-parameter counts:
  dismissed as exact-by-construction, test-only m... (continued)

8347 of 12508 branches covered (66.73%)

Branch coverage included in aggregate %.

24 of 52 new or added lines in 20 files covered. (46.15%)

105 existing lines in 9 files now uncovered.

50060 of 79502 relevant lines covered (62.97%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.46
/src/KeenEyes.Graph/Systems/GraphRenderSystem.cs
1
using System.Numerics;
2
using KeenEyes.Graph.Abstractions;
3
using KeenEyes.Graphics.Abstractions;
4

5
namespace KeenEyes.Graph;
6

7
/// <summary>
8
/// System that renders graph canvases, nodes, and connections.
9
/// </summary>
10
/// <remarks>
11
/// <para>
12
/// Renders the following elements:
13
/// <list type="bullet">
14
/// <item><description>Grid lines in the visible canvas area</description></item>
15
/// <item><description>Connections as bezier curves between ports</description></item>
16
/// <item><description>Nodes as rounded rectangles with headers and ports</description></item>
17
/// <item><description>Selection highlight on selected nodes</description></item>
18
/// <item><description>Selection box during box-select interaction</description></item>
19
/// <item><description>Port hover highlights during connection creation</description></item>
20
/// <item><description>Connection preview while dragging from a port</description></item>
21
/// </list>
22
/// </para>
23
/// </remarks>
24
public sealed class GraphRenderSystem : SystemBase, IGraphRenderer
25
{
26
    private I2DRenderer? renderer;
27
    private ITextRenderer? textRenderer;
28
    private PortRegistry? portRegistry;
29
    private NodeTypeRegistry? nodeTypeRegistry;
30
    private PortPositionCache? portCache;
31

32
    // Canvas screen bounds
33
    private Rectangle canvasBounds = new(0, 0, 1280, 720);
1✔
34

35
    // Current rendering context (set per-frame)
36
    private Vector2 currentOrigin;
37
    private float currentZoom = 1f;
1✔
38
    private Vector2 currentPan;
39

40
    // Colors
41
    private static readonly Vector4 gridColorMajor = new(0.3f, 0.3f, 0.3f, 1f);
×
42
    private static readonly Vector4 gridColorMinor = new(0.2f, 0.2f, 0.2f, 1f);
×
43
    private static readonly Vector4 nodeBodyColor = new(0.15f, 0.15f, 0.15f, 1f);
×
44
    private static readonly Vector4 nodeHeaderColor = new(0.25f, 0.35f, 0.5f, 1f);
×
45
    private static readonly Vector4 nodeBorderColor = new(0.4f, 0.4f, 0.4f, 1f);
×
46
    private static readonly Vector4 nodeSelectedBorderColor = new(0.3f, 0.6f, 1f, 1f);
×
47
    private static readonly Vector4 nodeGhostColor = new(0.15f, 0.15f, 0.15f, 0.5f);
×
48
    private static readonly Vector4 nodeGhostHeaderColor = new(0.25f, 0.35f, 0.5f, 0.5f);
×
49
    private static readonly Vector4 nodeGhostBorderColor = new(0.4f, 0.4f, 0.4f, 0.5f);
×
50
    private static readonly Vector4 portColor = new(0.8f, 0.8f, 0.8f, 1f);
×
51
    private static readonly Vector4 selectionBoxFillColor = new(0.3f, 0.6f, 1f, 0.15f);
×
52
    private static readonly Vector4 selectionBoxBorderColor = new(0.3f, 0.6f, 1f, 0.8f);
×
53
    private static readonly Vector4 portHighlightValidColor = new(0.3f, 0.9f, 0.3f, 1f);
×
54
    private static readonly Vector4 portHighlightInvalidColor = new(0.9f, 0.3f, 0.3f, 1f);
×
55
    private static readonly Vector4 connectionPreviewColor = new(0.7f, 0.7f, 0.7f, 0.6f);
×
56
    private static readonly Vector4 conversionIndicatorColor = new(1f, 0.8f, 0.2f, 1f);
×
57
    private static readonly Vector4 contextMenuBackgroundColor = new(0.15f, 0.15f, 0.15f, 0.95f);
×
58
    private static readonly Vector4 contextMenuBorderColor = new(0.5f, 0.5f, 0.5f, 1f);
×
59
    private static readonly Vector4 contextMenuItemHoverColor = new(0.3f, 0.4f, 0.6f, 1f);
×
60
    private static readonly Vector4 collapseButtonColor = new(0.7f, 0.7f, 0.7f, 1f);
×
61

62
    private const float NodeBorderRadius = 6f;
63
    private const float BorderThickness = 2f;
64
    private const float ConnectionThickness = 2f;
65
    private const float PortHighlightRadius = 10f;
66
    private const float PortHighlightThickness = 3f;
67
    private const float ConversionIndicatorRadius = 4f;
68
    private const float CollapseButtonSize = 12f;
69
    private const float CollapseButtonPadding = 8f;
70

71
    /// <summary>
72
    /// Sets the canvas screen bounds.
73
    /// </summary>
74
    /// <param name="bounds">The screen rectangle of the canvas area.</param>
75
    public void SetCanvasBounds(Rectangle bounds)
76
    {
77
        canvasBounds = bounds;
×
78
    }
×
79

80
    /// <inheritdoc />
81
    public override void Update(float deltaTime)
82
    {
83
        // Get renderer
84
        if (renderer is null && !World.TryGetExtension(out renderer))
×
85
        {
86
            return;
×
87
        }
88

89
        // Text renderer is optional
90
        if (textRenderer is null)
×
91
        {
92
            World.TryGetExtension(out textRenderer);
×
93
        }
94

95
        // Get port registry
96
        if (portRegistry is null && !World.TryGetExtension(out portRegistry))
×
97
        {
98
            return;
×
99
        }
100

101
        // Get node type registry
102
        if (nodeTypeRegistry is null)
×
103
        {
104
            World.TryGetExtension(out nodeTypeRegistry);
×
105
        }
106

107
        // Get port position cache
108
        if (portCache is null && !World.TryGetExtension(out portCache))
×
109
        {
110
            return;
×
111
        }
112

113
        // Render each canvas
114
        foreach (var canvas in World.Query<GraphCanvas, GraphCanvasTag>())
×
115
        {
116
            RenderCanvas(canvas);
×
117
        }
118
    }
×
119

120
    private void RenderCanvas(Entity canvas)
121
    {
122
        ref readonly var canvasData = ref World.Get<GraphCanvas>(canvas);
×
123
        var origin = new Vector2(canvasBounds.X, canvasBounds.Y);
×
124

125
        // Store current rendering context
126
        currentOrigin = origin;
×
127
        currentZoom = canvasData.Zoom;
×
128
        currentPan = canvasData.Pan;
×
129

130
        renderer!.Begin();
×
131

132
        try
133
        {
134
            // Clip to canvas bounds
135
            renderer.PushClip(canvasBounds);
×
136

137
            // Draw grid
138
            DrawGrid(canvasData, origin);
×
139

140
            // Draw connections
141
            DrawConnections(canvas, canvasData, origin);
×
142

143
            // Draw pending connection preview
144
            DrawPendingConnection(canvas, canvasData, origin);
×
145

146
            // Draw nodes
147
            DrawNodes(canvas, canvasData, origin);
×
148

149
            // Draw ghost nodes (duplication preview)
150
            DrawGhostNodes(canvas, canvasData, origin);
×
151

152
            // Draw port highlights
153
            DrawHoveredPort(canvas, canvasData, origin);
×
154

155
            // Draw selection box if selecting
156
            if (canvasData.Mode == GraphInteractionMode.Selecting)
×
157
            {
158
                DrawSelectionBox(canvasData);
×
159
            }
160

161
            // Draw context menu
162
            DrawContextMenu(canvas);
×
163

164
            renderer.PopClip();
×
165
        }
×
166
        finally
167
        {
168
            renderer.End();
×
169
        }
×
170
    }
×
171

172
    private void DrawGrid(in GraphCanvas canvasData, Vector2 origin)
173
    {
174
        var visibleArea = GraphTransform.GetVisibleArea(canvasBounds, canvasData.Pan, canvasData.Zoom);
×
175
        var gridSize = canvasData.GridSize;
×
176

177
        if (gridSize <= 0)
×
178
        {
179
            return;
×
180
        }
181

182
        // Calculate grid line range
183
        var startX = MathF.Floor(visibleArea.X / gridSize) * gridSize;
×
184
        var startY = MathF.Floor(visibleArea.Y / gridSize) * gridSize;
×
185
        var endX = visibleArea.X + visibleArea.Width;
×
186
        var endY = visibleArea.Y + visibleArea.Height;
×
187

188
        // Draw minor grid lines (every grid unit)
189
        for (var x = startX; x <= endX; x += gridSize)
×
190
        {
191
            var screenStart = GraphTransform.CanvasToScreen(new Vector2(x, visibleArea.Y), canvasData.Pan, canvasData.Zoom, origin);
×
192
            var screenEnd = GraphTransform.CanvasToScreen(new Vector2(x, visibleArea.Y + visibleArea.Height), canvasData.Pan, canvasData.Zoom, origin);
×
193

194
            // Major lines every 5 grid units
195
            var isMajor = MathF.Abs(x % (gridSize * 5)) < 0.1f;
×
196
            var color = isMajor ? gridColorMajor : gridColorMinor;
×
197

198
            renderer!.DrawLine(screenStart, screenEnd, color, 1f);
×
199
        }
200

201
        for (var y = startY; y <= endY; y += gridSize)
×
202
        {
203
            var screenStart = GraphTransform.CanvasToScreen(new Vector2(visibleArea.X, y), canvasData.Pan, canvasData.Zoom, origin);
×
204
            var screenEnd = GraphTransform.CanvasToScreen(new Vector2(visibleArea.X + visibleArea.Width, y), canvasData.Pan, canvasData.Zoom, origin);
×
205

206
            var isMajor = MathF.Abs(y % (gridSize * 5)) < 0.1f;
×
207
            var color = isMajor ? gridColorMajor : gridColorMinor;
×
208

209
            renderer!.DrawLine(screenStart, screenEnd, color, 1f);
×
210
        }
211
    }
×
212

213
    private void DrawConnections(Entity canvas, in GraphCanvas canvasData, Vector2 origin)
214
    {
215
        foreach (var connectionEntity in World.Query<GraphConnection>())
×
216
        {
217
            ref readonly var connection = ref World.Get<GraphConnection>(connectionEntity);
×
218
            if (connection.Canvas != canvas)
×
219
            {
220
                continue;
221
            }
222

223
            // Get source and target node positions
224
            if (!World.IsAlive(connection.SourceNode) || !World.IsAlive(connection.TargetNode))
×
225
            {
226
                continue;
227
            }
228

229
            ref readonly var sourceNode = ref World.Get<GraphNode>(connection.SourceNode);
×
230
            ref readonly var targetNode = ref World.Get<GraphNode>(connection.TargetNode);
×
231

232
            // Calculate port positions
233
            var sourcePortY = GraphLayoutSystem.HeaderHeight + (connection.SourcePortIndex * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
234
            var targetPortY = GraphLayoutSystem.HeaderHeight + (connection.TargetPortIndex * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
235

236
            var sourcePos = sourceNode.Position + new Vector2(sourceNode.Width, sourcePortY);
×
237
            var targetPos = targetNode.Position + new Vector2(0, targetPortY);
×
238

239
            // Convert to screen coordinates
240
            var screenStart = GraphTransform.CanvasToScreen(sourcePos, canvasData.Pan, canvasData.Zoom, origin);
×
241
            var screenEnd = GraphTransform.CanvasToScreen(targetPos, canvasData.Pan, canvasData.Zoom, origin);
×
242

243
            // Look up port types from the registry
244
            var sourceType = PortTypeId.Any;
×
245
            var targetType = PortTypeId.Any;
×
246
            if (portRegistry!.TryGetNodeType(sourceNode.NodeTypeId, out var sourceNodeType) &&
×
247
                connection.SourcePortIndex < sourceNodeType.OutputPorts.Length)
×
248
            {
249
                sourceType = sourceNodeType.OutputPorts[connection.SourcePortIndex].TypeId;
×
250
            }
251
            if (portRegistry.TryGetNodeType(targetNode.NodeTypeId, out var targetNodeType) &&
×
252
                connection.TargetPortIndex < targetNodeType.InputPorts.Length)
×
253
            {
254
                targetType = targetNodeType.InputPorts[connection.TargetPortIndex].TypeId;
×
255
            }
256

257
            // Check if type conversion is required
258
            var requiresConversion = sourceType != targetType
×
259
                && sourceType != PortTypeId.Any
×
260
                && targetType != PortTypeId.Any;
×
261

262
            // Draw connection curve
263
            var isSelected = World.Has<GraphConnectionSelectedTag>(connectionEntity);
×
264
            DrawConnection(screenStart, screenEnd, sourceType, ConnectionStyle.Bezier, isSelected, requiresConversion);
×
265
        }
266
    }
×
267

268
    private void DrawPendingConnection(Entity canvas, in GraphCanvas canvasData, Vector2 origin)
269
    {
270
        if (!World.Has<PendingConnection>(canvas))
×
271
        {
272
            return;
×
273
        }
274

275
        ref readonly var pending = ref World.Get<PendingConnection>(canvas);
×
276

277
        // Get source port position from cache
278
        var sourceDirection = pending.IsFromOutput ? PortDirection.Output : PortDirection.Input;
×
279
        if (portCache?.TryGetPortPosition(pending.SourceNode, sourceDirection, pending.SourcePortIndex, out var sourceCanvasPos) != true)
×
280
        {
281
            return;
×
282
        }
283

284
        var screenStart = GraphTransform.CanvasToScreen(sourceCanvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
285
        var screenEnd = GraphTransform.CanvasToScreen(pending.CurrentPosition, canvasData.Pan, canvasData.Zoom, origin);
×
286

287
        // If dragging from input, swap start and end
288
        if (!pending.IsFromOutput)
×
289
        {
290
            (screenStart, screenEnd) = (screenEnd, screenStart);
×
291
        }
292

293
        // Check for hovered target port type
294
        PortTypeId? targetType = null;
×
295
        if (World.Has<HoveredPort>(canvas))
×
296
        {
297
            ref readonly var hovered = ref World.Get<HoveredPort>(canvas);
×
298
            targetType = hovered.TypeId;
×
299
        }
300

301
        DrawConnectionPreview(screenStart, screenEnd, pending.SourceType, targetType, ConnectionStyle.Bezier);
×
302
    }
×
303

304
    private void DrawHoveredPort(Entity canvas, in GraphCanvas canvasData, Vector2 origin)
305
    {
306
        if (!World.Has<HoveredPort>(canvas))
×
307
        {
308
            return;
×
309
        }
310

311
        ref readonly var hovered = ref World.Get<HoveredPort>(canvas);
×
312
        var screenPos = GraphTransform.CanvasToScreen(hovered.Position, canvasData.Pan, canvasData.Zoom, origin);
×
313

314
        // Determine if this is a valid target when a connection is being created
315
        var isValidTarget = true;
×
316
        if (World.Has<PendingConnection>(canvas))
×
317
        {
318
            ref readonly var pending = ref World.Get<PendingConnection>(canvas);
×
319

320
            // Invalid if same direction (output to output or input to input)
321
            var targetDirection = hovered.Direction;
×
322
            var sourceDirection = pending.IsFromOutput ? PortDirection.Output : PortDirection.Input;
×
323
            if (targetDirection == sourceDirection)
×
324
            {
325
                isValidTarget = false;
×
326
            }
327
            // Invalid if same node
328
            else if (hovered.Node == pending.SourceNode)
×
329
            {
330
                isValidTarget = false;
×
331
            }
332
            // Check type compatibility
333
            else
334
            {
335
                isValidTarget = PortTypeCompatibility.CanConnect(pending.SourceType, hovered.TypeId);
×
336
            }
337
        }
338

339
        DrawPortHighlight(screenPos, hovered.TypeId, isValidTarget);
×
340
    }
×
341

342
    private void DrawNodes(Entity canvas, in GraphCanvas canvasData, Vector2 origin)
343
    {
344
        foreach (var node in World.Query<GraphNode>())
×
345
        {
346
            ref readonly var nodeData = ref World.Get<GraphNode>(node);
×
347
            if (nodeData.Canvas != canvas)
×
348
            {
349
                continue;
350
            }
351

352
            DrawNode(node, in nodeData, canvasData, origin);
×
353
        }
354
    }
×
355

356
    private void DrawNode(Entity node, ref readonly GraphNode nodeData, in GraphCanvas canvasData, Vector2 origin)
357
    {
358
        var nodeRect = new Rectangle(nodeData.Position.X, nodeData.Position.Y, nodeData.Width, nodeData.Height);
×
359
        var screenRect = GraphTransform.CanvasToScreen(nodeRect, canvasData.Pan, canvasData.Zoom, origin);
×
360

361
        var isSelected = World.Has<GraphNodeSelectedTag>(node);
×
362
        var isCollapsed = World.Has<GraphNodeCollapsed>(node);
×
363
        var borderColor = isSelected ? nodeSelectedBorderColor : nodeBorderColor;
×
364
        var scaledRadius = NodeBorderRadius * canvasData.Zoom;
×
365
        var headerHeight = GraphLayoutSystem.HeaderHeight * canvasData.Zoom;
×
366

367
        // Draw node body (only if not collapsed)
368
        if (!isCollapsed)
×
369
        {
370
            renderer!.FillRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, screenRect.Height, scaledRadius, nodeBodyColor);
×
371
        }
372

373
        // Draw header (full node when not collapsed, entire node when collapsed)
374
        renderer!.FillRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, isCollapsed ? screenRect.Height : headerHeight, scaledRadius, nodeHeaderColor);
×
375

376
        // Draw node border
377
        renderer.DrawRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, screenRect.Height, scaledRadius, borderColor, BorderThickness);
×
378

379
        // Draw collapse button if collapsible
380
        DrawCollapseButton(node, nodeData.NodeTypeId, screenRect, canvasData.Zoom, isCollapsed);
×
381

382
        // Note: Text rendering for node name would go here if textRenderer is available
383
        // For Phase 1, we skip text since it requires font setup
384

385
        // Draw ports (or port stubs when collapsed)
386
        if (isCollapsed)
×
387
        {
388
            DrawPortStubs(node, in nodeData, canvasData, origin);
×
389
        }
390
        else
391
        {
392
            DrawPorts(node, in nodeData, canvasData, origin);
×
393

394
            // Render custom body content if the node type supports it
395
            DrawNodeBody(node, nodeData.NodeTypeId, screenRect, canvasData.Zoom);
×
396
        }
397
    }
×
398

399
    private void DrawNodeBody(Entity node, int nodeTypeId, Rectangle screenRect, float zoom)
400
    {
401
        var definition = nodeTypeRegistry?.GetDefinition(nodeTypeId);
×
402
        if (definition is null)
×
403
        {
404
            return;
×
405
        }
406

407
        // Calculate body area below header and ports, above bottom padding
408
        if (!portRegistry!.TryGetNodeType(nodeTypeId, out var nodeType))
×
409
        {
410
            return;
×
411
        }
412

413
        var inputCount = nodeType.InputPorts.Length;
×
414
        var outputCount = nodeType.OutputPorts.Length;
×
415
        var maxPortCount = Math.Max(inputCount, outputCount);
×
416

417
        var headerHeight = GraphLayoutSystem.HeaderHeight * zoom;
×
418
        var portsHeight = maxPortCount * GraphLayoutSystem.PortRowHeight * zoom;
×
419
        var bottomPadding = GraphLayoutSystem.BottomPadding * zoom;
×
420

421
        var bodyTop = screenRect.Y + headerHeight + portsHeight;
×
422
        var bodyHeight = screenRect.Height - headerHeight - portsHeight - bottomPadding;
×
423

424
        if (bodyHeight <= 0)
×
425
        {
426
            return; // No space for body content
×
427
        }
428

429
        var bodyArea = new Rectangle(
×
430
            screenRect.X + BorderThickness,
×
431
            bodyTop,
×
432
            screenRect.Width - (BorderThickness * 2),
×
433
            bodyHeight);
×
434

435
        // Call the definition's RenderBody method
436
        definition.RenderBody(node, World, renderer!, bodyArea);
×
437
    }
×
438

439
    private void DrawPorts(Entity node, ref readonly GraphNode nodeData, in GraphCanvas canvasData, Vector2 origin)
440
    {
441
        if (!portRegistry!.TryGetNodeType(nodeData.NodeTypeId, out var nodeType))
×
442
        {
443
            return;
×
444
        }
445

446
        var portRadius = GraphLayoutSystem.PortRadius * canvasData.Zoom;
×
447

448
        // Draw input ports (left edge)
449
        for (int i = 0; i < nodeType.InputPorts.Length; i++)
×
450
        {
451
            var yOffset = GraphLayoutSystem.HeaderHeight + (i * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
452
            var canvasPos = nodeData.Position + new Vector2(0, yOffset);
×
453
            var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
454

455
            var port = nodeType.InputPorts[i];
×
NEW
456
            var typedPortColor = GetportColor(port.TypeId);
×
NEW
457
            renderer!.FillCircle(screenPos.X, screenPos.Y, portRadius, typedPortColor);
×
458
        }
459

460
        // Draw output ports (right edge)
461
        for (int i = 0; i < nodeType.OutputPorts.Length; i++)
×
462
        {
463
            var yOffset = GraphLayoutSystem.HeaderHeight + (i * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
464
            var canvasPos = nodeData.Position + new Vector2(nodeData.Width, yOffset);
×
465
            var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
466

467
            var port = nodeType.OutputPorts[i];
×
NEW
468
            var typedPortColor = GetportColor(port.TypeId);
×
NEW
469
            renderer!.FillCircle(screenPos.X, screenPos.Y, portRadius, typedPortColor);
×
470
        }
471
    }
×
472

473
    private void DrawCollapseButton(Entity node, int nodeTypeId, Rectangle screenRect, float zoom, bool isCollapsed)
474
    {
475
        // Check if node type is collapsible
476
        var definition = nodeTypeRegistry?.GetDefinition(nodeTypeId);
×
477
        if (definition is null || !definition.IsCollapsible)
×
478
        {
479
            return;
×
480
        }
481

482
        var buttonSize = CollapseButtonSize * zoom;
×
483
        var padding = CollapseButtonPadding * zoom;
×
484
        var headerHeight = GraphLayoutSystem.HeaderHeight * zoom;
×
485

486
        // Position button in right side of header
487
        var buttonX = screenRect.X + screenRect.Width - padding - buttonSize;
×
488
        var buttonY = screenRect.Y + ((headerHeight - buttonSize) / 2f);
×
489

490
        // Draw triangle indicator (pointing right when collapsed, down when expanded)
491
        var centerX = buttonX + (buttonSize / 2f);
×
492
        var centerY = buttonY + (buttonSize / 2f);
×
493
        var halfSize = buttonSize / 3f;
×
494

495
        Span<Vector2> triangle = stackalloc Vector2[3];
×
496

497
        if (isCollapsed)
×
498
        {
499
            // Right-pointing triangle (collapsed)
500
            triangle[0] = new Vector2(centerX - halfSize, centerY - halfSize);
×
501
            triangle[1] = new Vector2(centerX + halfSize, centerY);
×
502
            triangle[2] = new Vector2(centerX - halfSize, centerY + halfSize);
×
503
        }
504
        else
505
        {
506
            // Down-pointing triangle (expanded)
507
            triangle[0] = new Vector2(centerX - halfSize, centerY - halfSize);
×
508
            triangle[1] = new Vector2(centerX + halfSize, centerY - halfSize);
×
509
            triangle[2] = new Vector2(centerX, centerY + halfSize);
×
510
        }
511

512
        // Draw triangle indicator as outline (FillTriangle not available in I2DRenderer)
513
        renderer!.DrawPolygon(triangle, collapseButtonColor, 1.5f * zoom);
×
514
    }
×
515

516
    private void DrawPortStubs(Entity node, ref readonly GraphNode nodeData, in GraphCanvas canvasData, Vector2 origin)
517
    {
518
        if (!portRegistry!.TryGetNodeType(nodeData.NodeTypeId, out var nodeType))
×
519
        {
520
            return;
×
521
        }
522

523
        var stubRadius = GraphLayoutSystem.PortStubRadius * canvasData.Zoom;
×
524

525
        // Use cached port positions (already calculated for collapsed state by layout system)
526
        // Draw input port stubs
527
        for (int i = 0; i < nodeType.InputPorts.Length; i++)
×
528
        {
529
            if (portCache!.TryGetPortPosition(node, PortDirection.Input, i, out var canvasPos))
×
530
            {
531
                var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
532
                var port = nodeType.InputPorts[i];
×
NEW
533
                var typedPortColor = GetportColor(port.TypeId);
×
NEW
534
                renderer!.FillCircle(screenPos.X, screenPos.Y, stubRadius, typedPortColor);
×
535
            }
536
        }
537

538
        // Draw output port stubs
539
        for (int i = 0; i < nodeType.OutputPorts.Length; i++)
×
540
        {
541
            if (portCache!.TryGetPortPosition(node, PortDirection.Output, i, out var canvasPos))
×
542
            {
543
                var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
544
                var port = nodeType.OutputPorts[i];
×
NEW
545
                var typedPortColor = GetportColor(port.TypeId);
×
NEW
546
                renderer!.FillCircle(screenPos.X, screenPos.Y, stubRadius, typedPortColor);
×
547
            }
548
        }
549
    }
×
550

551
    private void DrawSelectionBox(in GraphCanvas canvasData)
552
    {
553
        // Selection box bounds would come from GraphInputSystem
554
        // For now, draw using canvas selection start/current position if available
555
        // This will be fully wired when input system exposes selection bounds
556
    }
×
557

558
    #region IGraphRenderer Implementation
559

560
    /// <inheritdoc />
561
    public void DrawConnection(
562
        Vector2 start,
563
        Vector2 end,
564
        PortTypeId type,
565
        ConnectionStyle style,
566
        bool isSelected,
567
        bool requiresConversion)
568
    {
569
        var color = isSelected ? nodeSelectedBorderColor : GetportColor(type);
×
570

571
        switch (style)
572
        {
573
            case ConnectionStyle.Bezier:
574
                DrawBezierConnection(start, end, color);
×
575
                break;
×
576
            case ConnectionStyle.Straight:
577
                renderer!.DrawLine(start, end, color, ConnectionThickness);
×
578
                break;
×
579
            case ConnectionStyle.Stepped:
580
                DrawSteppedConnection(start, end, color);
×
581
                break;
582
        }
583

584
        // Draw conversion indicator at midpoint if required
585
        if (requiresConversion)
×
586
        {
587
            var midpoint = (start + end) / 2f;
×
588
            renderer!.FillCircle(midpoint.X, midpoint.Y, ConversionIndicatorRadius * currentZoom, conversionIndicatorColor);
×
589
        }
590
    }
×
591

592
    /// <inheritdoc />
593
    public void DrawGrid(Rectangle visibleArea, float gridSize, float zoom)
594
    {
595
        if (gridSize <= 0)
×
596
        {
597
            return;
×
598
        }
599

600
        // Calculate grid line range
601
        var startX = MathF.Floor(visibleArea.X / gridSize) * gridSize;
×
602
        var startY = MathF.Floor(visibleArea.Y / gridSize) * gridSize;
×
603
        var endX = visibleArea.X + visibleArea.Width;
×
604
        var endY = visibleArea.Y + visibleArea.Height;
×
605

606
        // Draw minor grid lines (every grid unit)
607
        for (var x = startX; x <= endX; x += gridSize)
×
608
        {
609
            var screenStart = GraphTransform.CanvasToScreen(new Vector2(x, visibleArea.Y), currentPan, zoom, currentOrigin);
×
610
            var screenEnd = GraphTransform.CanvasToScreen(new Vector2(x, visibleArea.Y + visibleArea.Height), currentPan, zoom, currentOrigin);
×
611

612
            // Major lines every 5 grid units
613
            var isMajor = MathF.Abs(x % (gridSize * 5)) < 0.1f;
×
614
            var color = isMajor ? gridColorMajor : gridColorMinor;
×
615

616
            renderer!.DrawLine(screenStart, screenEnd, color, 1f);
×
617
        }
618

619
        for (var y = startY; y <= endY; y += gridSize)
×
620
        {
621
            var screenStart = GraphTransform.CanvasToScreen(new Vector2(visibleArea.X, y), currentPan, zoom, currentOrigin);
×
622
            var screenEnd = GraphTransform.CanvasToScreen(new Vector2(visibleArea.X + visibleArea.Width, y), currentPan, zoom, currentOrigin);
×
623

624
            var isMajor = MathF.Abs(y % (gridSize * 5)) < 0.1f;
×
625
            var color = isMajor ? gridColorMajor : gridColorMinor;
×
626

627
            renderer!.DrawLine(screenStart, screenEnd, color, 1f);
×
628
        }
629
    }
×
630

631
    /// <inheritdoc />
632
    public void DrawSelectionBox(Rectangle bounds)
633
    {
634
        renderer!.FillRect(bounds.X, bounds.Y, bounds.Width, bounds.Height, selectionBoxFillColor);
×
635
        renderer.DrawRect(bounds.X, bounds.Y, bounds.Width, bounds.Height, selectionBoxBorderColor, 1f);
×
636
    }
×
637

638
    /// <inheritdoc />
639
    public void DrawPortHighlight(Vector2 position, PortTypeId type, bool isValidTarget)
640
    {
641
        var color = isValidTarget ? portHighlightValidColor : portHighlightInvalidColor;
×
642
        var radius = PortHighlightRadius * currentZoom;
×
643

644
        // Draw highlight ring around port
645
        renderer!.DrawCircle(position.X, position.Y, radius, color, PortHighlightThickness);
×
646
    }
×
647

648
    /// <inheritdoc />
649
    public void DrawConnectionPreview(
650
        Vector2 start,
651
        Vector2 end,
652
        PortTypeId sourceType,
653
        PortTypeId? targetType,
654
        ConnectionStyle style)
655
    {
656
        var color = connectionPreviewColor;
×
657

658
        // If over a valid target, use the source type color with transparency
659
        if (targetType.HasValue && PortTypeCompatibility.CanConnect(sourceType, targetType.Value))
×
660
        {
661
            var typeColor = GetportColor(sourceType);
×
662
            color = new Vector4(typeColor.X, typeColor.Y, typeColor.Z, 0.6f);
×
663
        }
664

665
        switch (style)
666
        {
667
            case ConnectionStyle.Bezier:
668
                DrawBezierConnection(start, end, color);
×
669
                break;
×
670
            case ConnectionStyle.Straight:
671
                renderer!.DrawLine(start, end, color, ConnectionThickness);
×
672
                break;
×
673
            case ConnectionStyle.Stepped:
674
                DrawSteppedConnection(start, end, color);
×
675
                break;
676
        }
677
    }
×
678

679
    #endregion
680

681
    private void DrawBezierConnection(Vector2 start, Vector2 end, Vector4 color)
682
    {
683
        // Calculate control points for horizontal S-curve
684
        var (cp1, cp2) = BezierCurve.CalculateControlPoints(start, end);
×
685

686
        // Calculate adaptive segment count based on screen distance
687
        var segments = BezierCurve.CalculateSegmentCount(start, end, currentZoom);
×
688

689
        // Tessellate the bezier curve
690
        Span<Vector2> points = stackalloc Vector2[segments + 1];
×
691
        BezierCurve.TessellateInto(start, cp1, cp2, end, points);
×
692

693
        // Draw line strip
694
        renderer!.DrawLineStrip(points, color, ConnectionThickness);
×
695
    }
×
696

697
    private void DrawSteppedConnection(Vector2 start, Vector2 end, Vector4 color)
698
    {
699
        // Horizontal-vertical-horizontal stepped path
700
        var midX = (start.X + end.X) / 2f;
×
701

702
        Span<Vector2> points = stackalloc Vector2[4];
×
703
        points[0] = start;
×
704
        points[1] = new Vector2(midX, start.Y);
×
705
        points[2] = new Vector2(midX, end.Y);
×
706
        points[3] = end;
×
707

708
        renderer!.DrawLineStrip(points, color, ConnectionThickness);
×
709
    }
×
710

711
    private void DrawGhostNodes(Entity canvas, in GraphCanvas canvasData, Vector2 origin)
712
    {
713
        // Draw ghost preview nodes (for duplication)
714
        foreach (var node in World.Query<GraphNode, GraphNodeGhostTag>())
×
715
        {
716
            ref readonly var nodeData = ref World.Get<GraphNode>(node);
×
717
            if (nodeData.Canvas != canvas)
×
718
            {
719
                continue;
720
            }
721

722
            DrawGhostNode(node, in nodeData, canvasData, origin);
×
723
        }
724
    }
×
725

726
    private void DrawGhostNode(Entity node, ref readonly GraphNode nodeData, in GraphCanvas canvasData, Vector2 origin)
727
    {
728
        var nodeRect = new Rectangle(nodeData.Position.X, nodeData.Position.Y, nodeData.Width, nodeData.Height);
×
729
        var screenRect = GraphTransform.CanvasToScreen(nodeRect, canvasData.Pan, canvasData.Zoom, origin);
×
730

731
        var scaledRadius = NodeBorderRadius * canvasData.Zoom;
×
732

733
        // Draw semi-transparent node body
734
        renderer!.FillRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, screenRect.Height, scaledRadius, nodeGhostColor);
×
735

736
        // Draw semi-transparent header
737
        var headerHeight = GraphLayoutSystem.HeaderHeight * canvasData.Zoom;
×
738
        renderer.FillRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, headerHeight, scaledRadius, nodeGhostHeaderColor);
×
739

740
        // Draw semi-transparent border
741
        renderer.DrawRoundedRect(screenRect.X, screenRect.Y, screenRect.Width, screenRect.Height, scaledRadius, nodeGhostBorderColor, BorderThickness);
×
742

743
        // Draw ports (also semi-transparent)
744
        if (portRegistry!.TryGetNodeType(nodeData.NodeTypeId, out var nodeType))
×
745
        {
746
            var portRadius = GraphLayoutSystem.PortRadius * canvasData.Zoom;
×
747

748
            // Draw input ports
749
            for (int i = 0; i < nodeType.InputPorts.Length; i++)
×
750
            {
751
                var yOffset = GraphLayoutSystem.HeaderHeight + (i * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
752
                var canvasPos = nodeData.Position + new Vector2(0, yOffset);
×
753
                var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
754

755
                var port = nodeType.InputPorts[i];
×
NEW
756
                var typedPortColor = GetportColor(port.TypeId);
×
NEW
757
                var ghostportColor = new Vector4(typedPortColor.X, typedPortColor.Y, typedPortColor.Z, 0.5f);
×
UNCOV
758
                renderer.FillCircle(screenPos.X, screenPos.Y, portRadius, ghostportColor);
×
759
            }
760

761
            // Draw output ports
762
            for (int i = 0; i < nodeType.OutputPorts.Length; i++)
×
763
            {
764
                var yOffset = GraphLayoutSystem.HeaderHeight + (i * GraphLayoutSystem.PortRowHeight) + (GraphLayoutSystem.PortRowHeight / 2);
×
765
                var canvasPos = nodeData.Position + new Vector2(nodeData.Width, yOffset);
×
766
                var screenPos = GraphTransform.CanvasToScreen(canvasPos, canvasData.Pan, canvasData.Zoom, origin);
×
767

768
                var port = nodeType.OutputPorts[i];
×
NEW
769
                var typedPortColor = GetportColor(port.TypeId);
×
NEW
770
                var ghostportColor = new Vector4(typedPortColor.X, typedPortColor.Y, typedPortColor.Z, 0.5f);
×
UNCOV
771
                renderer.FillCircle(screenPos.X, screenPos.Y, portRadius, ghostportColor);
×
772
            }
773
        }
774
    }
×
775

776
    private void DrawContextMenu(Entity canvas)
777
    {
778
        if (!World.Has<GraphContextMenu>(canvas))
×
779
        {
780
            return;
×
781
        }
782

783
        ref readonly var menu = ref World.Get<GraphContextMenu>(canvas);
×
784

785
        // Menu dimensions
786
        const float menuWidth = 250f;
787
        const float itemHeight = 30f;
788
        const float padding = 10f;
789

790
        var items = GetContextMenuItems(menu);
×
791
        if (items.Count == 0)
×
792
        {
793
            return;
×
794
        }
795

796
        var menuHeight = (items.Count * itemHeight) + (padding * 2);
×
797
        var menuX = menu.ScreenPosition.X;
×
798
        var menuY = menu.ScreenPosition.Y;
×
799

800
        // Draw menu background
801
        renderer!.FillRect(menuX, menuY, menuWidth, menuHeight, contextMenuBackgroundColor);
×
802
        renderer.DrawRect(menuX, menuY, menuWidth, menuHeight, contextMenuBorderColor, 1f);
×
803

804
        // Draw menu items
805
        for (int i = 0; i < items.Count; i++)
×
806
        {
807
            var itemY = menuY + padding + (i * itemHeight);
×
808
            var isSelected = i == menu.SelectedIndex;
×
809

810
            // Draw hover highlight
811
            if (isSelected)
×
812
            {
813
                renderer.FillRect(menuX + 2, itemY, menuWidth - 4, itemHeight, contextMenuItemHoverColor);
×
814
            }
815

816
            // Note: Text rendering would go here if textRenderer is available
817
            // For now, just draw the selection highlight
818
        }
819

820
        // Draw search filter indicator if applicable
821
        if (menu.MenuType == ContextMenuType.Canvas && !string.IsNullOrEmpty(menu.SearchFilter))
×
822
        {
823
            // Draw search box at bottom (visual indicator only, no text yet)
824
            var searchY = menuY + menuHeight - 30f;
×
825
            renderer.FillRect(menuX + 5, searchY, menuWidth - 10, 25f, new Vector4(0.2f, 0.2f, 0.2f, 1f));
×
826
            renderer.DrawRect(menuX + 5, searchY, menuWidth - 10, 25f, contextMenuBorderColor, 1f);
×
827
        }
828
    }
×
829

830
    private List<string> GetContextMenuItems(in GraphContextMenu menu)
831
    {
832
        return menu.MenuType switch
×
833
        {
×
834
            ContextMenuType.Canvas => portRegistry?.GetAllNodeTypes().Select(t => t.Name).ToList() ?? [],
×
835
            ContextMenuType.Node => ["Delete", "Duplicate"],
×
836
            ContextMenuType.Connection => ["Delete"],
×
837
            _ => []
×
838
        };
×
839
    }
840

841
    private static Vector4 GetportColor(PortTypeId typeId)
842
    {
843
        return typeId switch
×
844
        {
×
845
            PortTypeId.Float or PortTypeId.Float2 or PortTypeId.Float3 or PortTypeId.Float4 => new Vector4(0.4f, 0.8f, 0.4f, 1f),
×
846
            PortTypeId.Int or PortTypeId.Int2 or PortTypeId.Int3 or PortTypeId.Int4 => new Vector4(0.3f, 0.5f, 0.8f, 1f),
×
847
            PortTypeId.Bool => new Vector4(0.8f, 0.3f, 0.3f, 1f),
×
848
            PortTypeId.Entity => new Vector4(0.8f, 0.6f, 0.2f, 1f),
×
849
            PortTypeId.Flow => new Vector4(1f, 1f, 1f, 1f),
×
850
            _ => portColor
×
851
        };
×
852
    }
853
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc