ktsu.ForceDirectedLayout 3.32.2

Prefix Reserved
dotnet add package ktsu.ForceDirectedLayout --version 3.32.2
                    
NuGet\Install-Package ktsu.ForceDirectedLayout -Version 3.32.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ktsu.ForceDirectedLayout" Version="3.32.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ktsu.ForceDirectedLayout" Version="3.32.2" />
                    
Directory.Packages.props
<PackageReference Include="ktsu.ForceDirectedLayout" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ktsu.ForceDirectedLayout --version 3.32.2
                    
#r "nuget: ktsu.ForceDirectedLayout, 3.32.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ktsu.ForceDirectedLayout@3.32.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ktsu.ForceDirectedLayout&version=3.32.2
                    
Install as a Cake Addin
#tool nuget:?package=ktsu.ForceDirectedLayout&version=3.32.2
                    
Install as a Cake Tool

ktsu.ForceDirectedLayout

NuGet License

ForceDirectedLayout settles a graph into a readable shape: bodies repel each other across the clear space between their bounding boxes, edges pull like springs between the points they actually attach at, gravity keeps the whole thing together, edges are pulled towards horizontal and steep ones splayed apart so a renderer's curves stay clear of the bodies at their ends, and overlaps are pushed apart. Two edges meeting at one node put their far ends into the same vertical order as the pins they arrive at, so they stop crossing each other. Edges that run the wrong way reorder themselves. Both untangles are given the axis they travel on: the overlap pass separates them on the other one, rather than holding a pair apart on the very axis its swap has to cross, so nothing is left drawn overlapping once an untangle is done. It is a pure simulation with no rendering, no UI dependency, and no runtime package dependencies — double precision throughout, AOT- and trim-clean, and exposed at three levels so a caller can pick how much ceremony they want. The same core is published as a native shared library for consumers outside .NET.

Features

  • Three surfaces over one core: a generic facade for your own body and edge types, a non-generic id-based facade for bulk POD submission, and the flat LayoutCore underneath
  • Renderer agnostic: nothing here knows what a node looks like; ktsu.ImGui.NodeEditor is one consumer, an Unreal plugin driving the C ABI is another
  • Step or solve: advance the simulation by a frame delta with automatic substepping, or run it to convergence with Solve(maxIterations, tolerance)
  • Stability reporting: total system energy, an IsStable flag, and what the last step actually ran (substep count and substep delta)
  • Pinning and freezing: a pinned body still pushes on others but does not move; a frozen body is one the user is currently dragging
  • Boxes, not points: bodies have dimensions, and the layout uses them — repulsion is measured between the two closest points on a pair's bounding boxes, so the same setting leaves the same room between a pair of literals as between a pair of classes, and an overlap pass separates any boxes that still end up drawn over one another
  • AOT and trim clean: IsAotCompatible, IsTrimmable, and analyzers enabled, with blittable POD settings and state structs
  • A C ABI: ForceDirectedLayout.Native publishes a Native AOT shared library (ktsu_force_directed_layout) with a Layout_* entry point set and a generated ktsu_force_directed_layout.h

Installation

Package Manager Console

Install-Package ktsu.ForceDirectedLayout

.NET CLI

dotnet add package ktsu.ForceDirectedLayout

Package Reference

<PackageReference Include="ktsu.ForceDirectedLayout" Version="x.y.z" />

Usage Examples

Basic Example — your own types

ForceDirectedLayout<TBody, TEdge> reads and writes your types through two accessor records, so the simulation never owns your model. WithPhysicsState returns the body with new position, velocity and force, which suits immutable records as readily as mutable classes.

using ktsu.ForceDirectedLayout;

BodyAccessor<MyNode> bodies = new(
    GetId: n => n.Id,
    GetPosition: n => new Vec2D(n.X, n.Y),
    GetDimensions: n => new Vec2D(n.Width, n.Height),
    GetVelocity: n => n.Velocity,
    GetForce: n => n.Force,
    GetIsPinned: n => n.IsPinned,
    WithPhysicsState: (n, position, velocity, force) => n with
    {
        X = position.X,
        Y = position.Y,
        Velocity = velocity,
        Force = force,
    });

EdgeAccessor<MyEdge> edges = new(
    GetSourceBodyId: e => e.From,
    GetTargetBodyId: e => e.To);

ForceDirectedLayout<MyNode, MyEdge> layout = new(bodies, edges)
{
    Settings = new PhysicsSettings { Enabled = true },
};

// Once per frame
layout.SetFrozenBodies(draggedNodeIds);
layout.Step(nodes, links, deltaTime);

Id-based submission

ForceLayout takes plain structs and hands back positions as a span, which is the shape a bulk producer or an interop caller wants.

ForceLayout layout = new();

layout.SetNodes([
    new NodeInit { Id = 1, Position = new Vec2D(0, 0), Dimensions = new Vec2D(120, 60) },
    new NodeInit { Id = 2, Position = new Vec2D(300, 40), Dimensions = new Vec2D(120, 60) },
]);

layout.SetEdges([new EdgeInit { SourceBodyId = 1, TargetBodyId = 2 }]);

int iterations = layout.Solve(maxIterations: 500, tolerance: 0.5);

foreach (NodePosition node in layout.GetPositionsView())
{
    Console.WriteLine($"{node.Id}: {node.Position.X}, {node.Position.Y}");
}

Tuning

Every force is a setting, and the defaults are tuned for node-editor-sized graphs.

PhysicsSettings settings = new()
{
    Enabled = true,
    RepulsionStrength = 600_000.0,     // inverse-square in the clear space between bounding boxes
    LinkSpringStrength = 0.5,          // Hooke's-law constant for edges
    RestLinkLength = 225.0,            // spring rest length
    DirectionalBias = 0.5,             // orders sources left of targets, reordering when needed
    LinkFlatteningStrength = 0.5,      // pulls edges towards horizontal, and keeps curves visible
    LinkFlatteningMargin = 0.0,        // extra clearance on top of the derived bound
    LinkUntwistStrength = 0.1,         // swaps two links sharing a node into their pins' order
    GravityStrength = 50.0,            // pull toward the gravity target
    OriginAnchorWeight = 1.0,          // 0 = centroid, 1 = world origin
    DampingFactor = 0.5,               // velocity retained per second
    MinRepulsionDistance = 50.0,       // floor on that clear space, so touching bodies push hard, not infinitely hard
    MaxForce = 5000.0,
    MaxVelocity = 250.0,             // also bounds how fast a graph settles
    TargetPhysicsHz = 120.0,           // substep rate, independent of frame rate
    StabilityThreshold = 1.0,
    OverlapMargin = 20.0,
    MaxOverlapCorrection = 40.0,
};

These values were not guessed. tests/ForceDirectedLayout.Tests/Bench/ settles a corpus of graphs over a range of starting arrangements and reports what a layout measures — settled area, mean edge angle, links drawn across a body they are no end of, tightest clear gap, overlaps, crossed link pairs — because the simulation is chaotic and a single run says nothing. LayoutBench.Sweep walks one setting across a range and prints the rows as a table, LayoutBench.Compare puts named variants side by side, and LayoutSvg writes a settled graph out as SVG so it can be looked at rather than only read. Changing what a force measures changes the units its strength is in, so that is how a new default gets found.

From native code

ForceDirectedLayout.Native publishes a shared library with a C entry point set — Layout_Create, Layout_Destroy, Layout_SetSettings, Layout_SetNodes, Layout_SetEdges, Layout_Step, Layout_Solve, Layout_GetPositions, Layout_SetPinned, Layout_GetIndexOf, Layout_GetNodeCount and Layout_GetLastErrorMessage — and ships ktsu_force_directed_layout.h beside the binary. The settings and node/edge structs are laid out sequentially and cross the ABI unchanged.

dotnet publish ForceDirectedLayout.Native -c Release -r win-x64

API Reference

ForceDirectedLayout<TBody, TEdge>

Name Return Type Description
Settings PhysicsSettings The managed-facing settings; mutate freely between frames
SetFrozenBodies(IReadOnlySet<int>) void Bodies excluded from integration, typically the ones being dragged
InitializeWorldOriginToCentroid(IReadOnlyList<TBody>) void Anchors the world origin to the current centroid
Step(IList<TBody>, IReadOnlyList<TEdge>, double) void Advances the simulation and writes state back through the accessor
LastStepInfo (int SubstepCount, double SubstepDeltaTime) What the last step actually ran

ForceLayout

Name Return Type Description
SetNodes(ReadOnlySpan<NodeInit>) / SetEdges(ReadOnlySpan<EdgeInit>) void Bulk submission of POD state
Step(double) void Advances by a delta, substepping to TargetPhysicsHz
Solve(int, double) int Runs to convergence, returning the iterations used
GetPositions(Span<NodePosition>) int Copies positions into a caller buffer
GetPositionsView() ReadOnlySpan<NodePosition> Zero-copy view of the current positions
SetPinned(int, bool) / SetFrozen(int, bool) void Pins or freezes a body by index
SetPosition(int, Vec2D) void Moves a body directly
GetIndexOf(int) int Index of a body id, or -1
InitializeWorldOriginToCentroid() void Anchors the world origin to the current centroid

LayoutCore

The flat working buffers under both facades: Settings, WorldOrigin, GravityCenter, TotalSystemEnergy, IsStable, LastStepInfo, Bodies, Edges, ResizeBodies, ResizeEdges, Step and Solve. Use it when you want to own the buffers yourself.

Supporting types

Vec2D (double-precision vector with the usual operators), BodyState, EdgeRef, NodeInit, EdgeInit, NodePosition, LayoutSettings (blittable POD, the C ABI form) and PhysicsSettings (the managed form, convertible both ways).

Acknowledgments

This package has no runtime dependencies; Polyfill is used at build time only, to backport newer APIs.

Contributing

Contributions are welcome! For feature requests, bug reports, or questions, please open an issue on the GitHub repository. If you would like to contribute code, please open a pull request with your changes.

License

ForceDirectedLayout is licensed under the MIT License. See LICENSE.md for more information.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on ktsu.ForceDirectedLayout:

Package Downloads
ktsu.ImGuiNodeEditor

A comprehensive .NET library suite for building desktop applications with Dear ImGui. Provides application scaffolding with PID-controlled frame limiting, custom widgets (TabPanel, SearchBox, Knob, RadialProgressBar, DividerContainer, Grid), modal dialogs (file browser, input prompts, searchable lists), a theming system with 50+ built-in themes and scoped styling, and an attribute-based node graph editor with physics-based layout. Built on Hexa.NET.ImGui bindings and Silk.NET for cross-platform windowing.

ktsu.Coder.Graph

A flexible and extensible .NET library for representing code as language-agnostic Abstract Syntax Trees, serializing them to human-readable YAML for round-trip storage and version control, and generating source in C#, Python, JavaScript and C++. Provides strongly-typed AST nodes for classes, functions, parameters, statements, expressions and typed literals, with a plugin-based architecture for adding target languages, full deep-cloning support and custom metadata on any node. Ships an ImGui node-graph editor over the same AST, with a force-directed layout, an inspector for every node's properties, undo/redo, and a desktop application built on it.

ktsu.ImGui.NodeEditor

A visual node editor for Dear ImGui built on ImNodes, with the graph kept away from the drawing: the engine owns nodes, links and layout and knows nothing about ImGui, while the renderer draws what it holds and the input handler turns interactions into requests the engine can accept or refuse. Nodes can be declared as ordinary types decorated with ktsu.NodeGraph attributes and instantiated by reflection, with connections checked against the rules that metadata declares. Optional force-directed layout settles the graph, and the view zooms from quarter to double scale.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.32.2 0 9/10/2026
3.32.1 58 9/9/2026
3.32.0 64 9/9/2026
3.31.0 72 9/9/2026
3.30.0 75 9/9/2026
3.29.0 69 9/9/2026
3.28.0 78 9/9/2026
3.27.0 78 9/9/2026
3.26.1 89 9/8/2026
3.26.0 72 9/8/2026
3.25.0 83 9/8/2026
3.24.0 72 9/8/2026
3.23.0 76 9/8/2026
3.22.0 78 9/8/2026
3.21.0 112 9/8/2026
3.20.0 82 9/8/2026
3.19.0 104 9/8/2026
3.18.0 125 9/8/2026
3.17.0 54 9/8/2026
3.16.11 91 9/7/2026
Loading failed

## v3.32.2 (patch)

Changes since v3.32.1:

- Bump Polyfill from 11.2.0 to 11.3.0 ([@dependabot[bot]](https://github.com/dependabot[bot]))
- Bump the ktsu group with 6 updates ([@dependabot[bot]](https://github.com/dependabot[bot]))