Skip to content

Extract force-directed layout into ktsu.ForceDirectedLayout#176

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/eloquent-newton-rKRcu
May 27, 2026
Merged

Extract force-directed layout into ktsu.ForceDirectedLayout#176
matt-edmondson merged 1 commit into
mainfrom
claude/eloquent-newton-rKRcu

Conversation

@matt-edmondson
Copy link
Copy Markdown
Contributor

Summary

Promotes the force-directed graph layout out of ImGuiNodeEditor into a new standalone, renderer-agnostic library ktsu.ForceDirectedLayout. The new library has no dependency on ImGui, ImNodes, nodes, pins, or any rendering — it operates on arbitrary caller-defined body and edge types via accessor adapters.

What moved

From ImGuiNodeEditor/NodeEditorEngine.cs and DomainModels.cs:

  • PhysicsSettings record — now in ktsu.ForceDirectedLayout namespace
  • Force calculations: repulsion, link springs, directional bias, gravity
  • Integration: damping, force/velocity clamping, position update
  • Position-based directional constraints
  • Stability detection (total system energy, threshold)
  • Substep scheduling (per-frame substep count from target physics Hz)
  • World origin / gravity-target computation

New public API

public class ForceDirectedLayout<TBody, TEdge>
{
    ForceDirectedLayout(BodyAccessor<TBody>, EdgeAccessor<TEdge>);
    PhysicsSettings Settings { get; set; }
    Vector2 WorldOrigin { get; set; }
    Vector2 GravityCenter { get; }
    bool IsStable { get; }
    float TotalSystemEnergy { get; }
    (int SubstepCount, float SubstepDeltaTime) LastStepInfo { get; }
    void SetFrozenBodies(IReadOnlySet<int>);
    void InitializeWorldOriginToCentroid(IReadOnlyList<TBody>);
    void Step(IList<TBody>, IReadOnlyList<TEdge>, float deltaTime);
}

public sealed record BodyAccessor<TBody>(
    Func<TBody, int> GetId,
    Func<TBody, Vector2> GetPosition,
    Func<TBody, Vector2> GetDimensions,
    Func<TBody, Vector2> GetVelocity,
    Func<TBody, Vector2> GetForce,
    Func<TBody, bool> GetIsPinned,
    Func<TBody, Vector2, Vector2, Vector2, TBody> WithPhysicsState);

public sealed record EdgeAccessor<TEdge>(
    Func<TEdge, int> GetSourceBodyId,
    Func<TEdge, int> GetTargetBodyId);

The simulation snapshots body state into an internal mutable working buffer to avoid per-substep record allocations, runs N substeps, then commits results back through WithPhysicsState. This is actually a small perf improvement over the previous node with { ... }-per-substep code path.

NodeEditorEngine

Public API is preserved. NodeEditorEngine now holds a ForceDirectedLayout<Node, Link> and proxies PhysicsSettings, GravityCenter, WorldOrigin, LastPhysicsStepInfo, TotalSystemEnergy, IsStable, UpdatePhysics, UpdatePhysicsSettings, SetDraggedNodes, and InitializeWorldOriginToCentroid to it. The EdgeAccessor resolves Link.OutputPinId/InputPinId to node ids via a pin→node-id map rebuilt at the top of each UpdatePhysics call.

One tiny API tightening: GravityCenter was previously { get; set; } and is now read-only (no caller in the tree set it — it was always overwritten by the next physics step).

Consumer impact

Consumers that referenced PhysicsSettings directly need to add using ktsu.ForceDirectedLayout;. Updated in this PR:

  • examples/ImGuiAppDemo/Demos/CleanImNodesDemo.cs

Test plan

  • dotnet build succeeds on the full solution in the project's CI environment
  • Run examples/ImGuiAppDemo → Clean ImNodes tab → enable physics and confirm nodes settle into a stable left-to-right layout
  • Toggle the "Gentle Physics" and "Strong Physics" presets and confirm the simulation responds
  • Drag a node and confirm it is frozen (no integration) while dragged
  • Pin a node (ToggleNodePinned) and confirm it stays put while others move
  • Confirm IsStable flips to true once the system settles
  • Confirm debug overlays (gravity center, force/velocity vectors, link stress colors) still render correctly

Generated by Claude Code

Move the force-directed physics simulation (repulsion, spring, gravity,
directional bias, integration) out of NodeEditorEngine into a new
standalone, renderer-agnostic library. The new library is generic over
caller-defined body and edge types via BodyAccessor<TBody>/EdgeAccessor<TEdge>
adapters, so it has no coupling to nodes, pins, ImNodes, or ImGui.

NodeEditorEngine keeps its public API: PhysicsSettings, GravityCenter,
WorldOrigin, UpdatePhysics, UpdatePhysicsSettings, LastPhysicsStepInfo,
TotalSystemEnergy, IsStable, SetDraggedNodes, InitializeWorldOriginToCentroid.
Internally it delegates to ForceDirectedLayout<Node, Link>, mapping pin ids
to body ids in the edge accessor.

PhysicsSettings moves to the ktsu.ForceDirectedLayout namespace; consumers
that referenced it directly need to add `using ktsu.ForceDirectedLayout;`.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
7 Security Hotspots
0.0% Coverage on New Code (required ≥ 80%)
C Reliability Rating on New Code (required ≥ A)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@matt-edmondson matt-edmondson merged commit 0e6f6d6 into main May 27, 2026
4 of 5 checks passed
@matt-edmondson matt-edmondson deleted the claude/eloquent-newton-rKRcu branch May 27, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants