ktsu.ForceDirectedLayout 3.54.0

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package ktsu.ForceDirectedLayout --version 3.54.0
                    
NuGet\Install-Package ktsu.ForceDirectedLayout -Version 3.54.0
                    
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.54.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ktsu.ForceDirectedLayout" Version="3.54.0" />
                    
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.54.0
                    
#r "nuget: ktsu.ForceDirectedLayout, 3.54.0"
                    
#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.54.0
                    
#: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.54.0
                    
Install as a Cake Addin
#tool nuget:?package=ktsu.ForceDirectedLayout&version=3.54.0
                    
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 = 900_000.0,     // inverse-square in the clear space between bounding boxes
    LinkSpringStrength = 0.1,          // Hooke's-law constant for edges
    RestLinkLength = 50.0,             // spring rest length
    DirectionalBias = 4.0,             // orders sources left of targets, reordering when needed
    LinkFlatteningStrength = 3.0,      // 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 = 5.0,        // floor on that clear space
    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,
};

MinRepulsionDistance is additionally floored internally above zero (MinimumRepulsionClamp) so inverse-square repulsion remains finite even if a caller sets it to zero.

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.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. Every edit can be undone through a ktsu.UndoRedo history, keyboard commands follow a ktsu.Keybinding keymap, dragged nodes can snap to the grid, and labelled comment boxes group regions of the graph and carry their nodes when moved.

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.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.75.2-pre.1 0 10/16/2026
3.75.1 131 10/14/2026
3.75.0 190 10/12/2026
3.74.0 138 10/12/2026
3.73.0 213 10/11/2026
3.72.1 180 10/11/2026
3.72.0 102 10/11/2026
3.71.0 138 10/11/2026
3.70.0 128 10/11/2026
3.69.1 104 10/11/2026
3.69.0 103 10/10/2026
3.68.0 100 10/10/2026
3.67.0 109 10/10/2026
3.66.0 119 10/10/2026
3.65.0 125 10/10/2026
3.64.2-pre.1 60 10/9/2026
3.64.1 108 10/9/2026
3.64.1-pre.1 62 10/8/2026
3.64.0 176 10/8/2026
3.54.0 165 9/29/2026
Loading failed

## v3.54.0 (minor)

Changes since v3.53.0:

- Merge main into the data table branch and give DataTable a gallery tile ([@matt-edmondson](https://github.com/matt-edmondson))
- Check the curve editor's drawn bounds without a nullable dereference ([@Claude](https://github.com/Claude))
- Compare floats with tolerances where the code-quality bot flagged them ([@Claude](https://github.com/Claude))
- [patch] Show the data table demo once, with the advanced demos ([@matt-edmondson](https://github.com/matt-edmondson))
- Regenerate the widget gallery with the fixed renderer and widgets ([@Claude](https://github.com/Claude))
- Fix four widgets that drew wrongly in the gallery ([@Claude](https://github.com/Claude))
- Make the software rasterizer follow the GPU rules ImGui relies on ([@Claude](https://github.com/Claude))
- [patch] Read an empty clipboard without throwing and address code quality findings ([@matt-edmondson](https://github.com/matt-edmondson))
- Merge main into the data table branch ([@matt-edmondson](https://github.com/matt-edmondson))
- Merge main into the data table branch ([@Claude](https://github.com/Claude))
- [patch] Drop a semicolon from the data table's Key Files line ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Skip parts already in stock when the demo marks them in stock ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Drive the data table through a keymap in the UI tests ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Require Shift for a chord that names it even with Shift ignored ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Begin a data table edit from a character typed with AltGr ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Report no edit for a null string committed unchanged ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Let a caller set a data table's selection and active cell ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Rebuild a data table over a list replaced after Refresh ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Commit a data table edit on keypad Enter and keep the edited row drawn ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Give the data table's clipper the row pitch with cell padding ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] State which frameworks ktsu.Keybinding brings System.Text.Json to ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Show the data table in the widgets demo and document it ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Open a caller-drawn context menu on a data table cell ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Leave the clipboard as found and ignore characters a cell can't start from ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Drive a data table from the keyboard and edit cells in place ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Filter a data table's columns from a row under the headers ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Draw a data table with sortable headers and clickable cells ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Declare the data table's keyboard commands for ktsu.Keybinding ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Edit one data table cell at a time and report each edit ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Move a data table's active cell and select its rows ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Sort and filter a data table's rows into a view ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Document the data table edit sessions' members ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add typed data table columns and their edit sessions ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Share the key chord matcher and let held chords repeat ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Plan the data table widget implementation ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Design a data table widget with sorting, filtering, and cell editing ([@matt-edmondson](https://github.com/matt-edmondson))