Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
defe4d2
[patch] Design node parameter editing
matt-edmondson Sep 23, 2026
c31974c
[patch] Plan node parameter editing
matt-edmondson Sep 23, 2026
91ff79c
[minor] Let a pin declare the type it carries
matt-edmondson Sep 23, 2026
8767786
[minor] Hold a value and a default per pin
matt-edmondson Sep 23, 2026
d548f3b
[minor] Give the engine a value per pin
matt-edmondson Sep 23, 2026
baeb56e
[minor] Carry a declared pin's type and default onto the graph
matt-edmondson Sep 23, 2026
ec4b2c5
[minor] Name the pin types that can be edited
matt-edmondson Sep 23, 2026
1cf8512
[minor] Draw an editor on an unconnected input pin
matt-edmondson Sep 23, 2026
4a33e89
[minor] Report which nodes are selected
matt-edmondson Sep 23, 2026
084e68b
[minor] Draw one node's parameters as a property grid
matt-edmondson Sep 23, 2026
b5f6d3e
[patch] Fix enum rows in the node inspector silently discarding edits
matt-edmondson Sep 23, 2026
f6ddeb2
[patch] Show parameter editing in the demo and the README
matt-edmondson Sep 23, 2026
a9d9255
Fix output pin labels detaching from their pins with inline editors
matt-edmondson Sep 23, 2026
ea6eb79
Coerce a mistyped declared pin default before seeding it
matt-edmondson Sep 23, 2026
8b8b99c
Document the parameter-editing surface and its dependency cost in CLA…
matt-edmondson Sep 23, 2026
7d8ff5f
Amend the design spec to name the shipped CreateNodeFromSpecs
matt-edmondson Sep 23, 2026
c260407
Fix enum editors silently misrepresenting unmatched or null values
matt-edmondson Sep 23, 2026
e531f93
Cheap wording and cleanup fixes from the branch review
matt-edmondson Sep 23, 2026
d0013d7
Merge origin/main into node parameter editing
matt-edmondson Sep 23, 2026
89d3820
Give a pin one home, whether or not it has an instance
matt-edmondson Sep 23, 2026
aded644
Drop two constant conditions the quality bot flagged
matt-edmondson Sep 23, 2026
5fa0b37
Drive every pin value kind on both editing surfaces
matt-edmondson Sep 23, 2026
8af891f
Describe both homes a parameter can have
matt-edmondson Sep 23, 2026
9e6bf83
Merge main into the node-parameters branch
matt-edmondson Sep 23, 2026
5946479
Merge the branch's own remote, which had gained a second merge of main
matt-edmondson Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ PublishScripts/
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# and except a Unity project's Packages/, which is source: Unity's package manifest and its
# resolved lock file are both meant to be committed, and a NuGet restore folder never contains
# a file by either name.
!**/[Pp]ackages/manifest.json
!**/[Pp]ackages/packages-lock.json
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
Expand Down Expand Up @@ -651,3 +656,16 @@ Temporary Items

# ImGui.ini files
imgui.ini

# Game engine projects
#
# Godot: the import cache, and the mono/temp bin+obj a C# build writes.
.godot/

# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule
# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs
# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently
# breaks those references - including for a plug-in whose .dll is itself a build output. This
# negation has to come after that rule to win, and is scoped to the asset tree so the Visual
# Studio artifact stays ignored everywhere else.
!**/[Aa]ssets/**/*.meta
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This is the **ktsu ImGui Suite**, a collection of .NET libraries for building De
- **ImGui.Styler** (`ktsu.ImGui.Styler`) - Theming system with 50+ built-in themes, scoped styling, Button.Alignment, Text.Color semantic colors, Indent utilities, Alignment helpers, theme-aware color palette (`Palette`, e.g. `Palette.Basic.Red`, `Palette.Semantic.Error`), and interactive theme browser. Color construction and manipulation live in `ImGui.Color`.
- **NodeGraph** (`ktsu.NodeGraph`) - UI-agnostic attribute-based node graph metadata: `[Node]`, `[InputPin]`, `[OutputPin]`, `[NodeExecute]`, `[NodeBehavior]`, pin type utilities
- **ForceDirectedLayout** (`ktsu.ForceDirectedLayout`) - Renderer-agnostic graph layout simulation, with no UI dependency and no runtime package dependencies. Bodies repel across the clear space between their bounding boxes (not between their centres — see [Layout benchmarking](#layout-benchmarking)), edges pull like springs between the pins they actually attach at, gravity holds the graph together, edges are pulled towards horizontal, an overlap pass separates any boxes left drawn over one another, and a recentring pass slides the whole arrangement so its drawn box sits on the world origin (see [Placement is not cohesion](#placement-is-not-cohesion)). Three surfaces over one `LayoutCore`: a generic facade over your own types, an id-based `ForceLayout` for bulk POD submission, and the flat core. Also published as a Native AOT shared library with a C ABI. `ImGui.NodeEditor` is one consumer.
- **ImGui.NodeEditor** (`ktsu.ImGui.NodeEditor`) - ImNodes-based visual node editor with `NodeEditorEngine`, `AttributeBasedNodeFactory`, physics-based layout, `NodeEditorRenderer`, `NodeEditorInputHandler`. `PhysicsSettingsPanel.Draw(ref PhysicsSettings)` draws every layout setting grouped by force and captioned, and `DrawDiagnostics(engine)` the live energy and settled state, so a consuming application gets the whole tuning surface rather than reimplementing a subset of it. ImNodes has no zoom of its own, so `NodeEditorRenderer.Zoom` supplies one and `FitToView` centres a graph and picks the zoom it fits at; the engine's positions and sizes stay at their own scale throughout, since that is the space the layout's lengths are measured in. Hovering is answered by the renderer: `HighlightLinksOnNodeHover` (on) colours the links meeting the hovered node, `HighlightDownstreamOnNodeHover` (off) also colours everything that node's value reaches, and `DrawHoveredLinkOnTop` (on) redraws the hovered link over the nodes ImNodes drew on top of it. See [Hover highlighting](#hover-highlighting) below. How many links a pin accepts is the pin's own business: `Pin.AllowsMultipleConnections` defaults to many for an output and one for an input, `[InputPin(AllowMultipleConnections = true)]` / `[OutputPin(AllowMultipleConnections = false)]` override it through the factory, and `NodeEditorEngine.SetPinAllowsMultipleConnections` sets it directly. `GetOutgoingLinks`, `GetIncomingLinks`, `GetDownstream` and `GetUpstream` walk the graph
- **ImGui.NodeEditor** (`ktsu.ImGui.NodeEditor`) - ImNodes-based visual node editor with `NodeEditorEngine`, `AttributeBasedNodeFactory`, physics-based layout, `NodeEditorRenderer`, `NodeEditorInputHandler`. `PhysicsSettingsPanel.Draw(ref PhysicsSettings)` draws every layout setting grouped by force and captioned, and `DrawDiagnostics(engine)` the live energy and settled state, so a consuming application gets the whole tuning surface rather than reimplementing a subset of it. ImNodes has no zoom of its own, so `NodeEditorRenderer.Zoom` supplies one and `FitToView` centres a graph and picks the zoom it fits at; the engine's positions and sizes stay at their own scale throughout, since that is the space the layout's lengths are measured in. Hovering is answered by the renderer: `HighlightLinksOnNodeHover` (on) colours the links meeting the hovered node, `HighlightDownstreamOnNodeHover` (off) also colours everything that node's value reaches, and `DrawHoveredLinkOnTop` (on) redraws the hovered link over the nodes ImNodes drew on top of it. See [Hover highlighting](#hover-highlighting) below. How many links a pin accepts is the pin's own business: `Pin.AllowsMultipleConnections` defaults to many for an output and one for an input, `[InputPin(AllowMultipleConnections = true)]` / `[OutputPin(AllowMultipleConnections = false)]` override it through the factory, and `NodeEditorEngine.SetPinAllowsMultipleConnections` sets it directly. `GetOutgoingLinks`, `GetIncomingLinks`, `GetDownstream` and `GetUpstream` walk the graph. A node's parameters are editable, not only its pins' connections: `Pin.DataType` carries the .NET type a pin was declared with (null for an untyped pin, which is what the plain-name `CreateNode` overloads still produce), and `PinValueStore` holds a value per pin id, seeded from a `PinSpec.DefaultValue` when `NodeEditorEngine.CreateNodeFromSpecs(Vector2, string, IReadOnlyList<PinSpec>, IReadOnlyList<PinSpec>)` creates the node (`PinSpec` is `(Name, DataType, DefaultValue, AllowMultipleConnections)`). That method is deliberately not a third `CreateNode` overload: a `CreateNode` taking `IReadOnlyList<PinSpec>` is ambiguous against the existing `List<string>` overload for a `[]` collection-expression argument, which broke the build. `PinValueKinds.Classify(Type?)` is the single classifier both editing surfaces consult (`Boolean`, `Int32`, `Single`, `Double`, `String`, `Vector2`, `Vector3`, `Enum`, or `Unsupported`; `long` is deliberately `Unsupported`, since Dear ImGui has no `InputLong`), so a type has an editor on both surfaces or neither, never one and not the other. The two surfaces: `NodeEditorRenderer` draws one inline, on an unconnected input pin's own row, gated on `DrawInlinePinEditors` (on by default) and `InlineEditorWidth`; and `NodeInspectorPanel.Draw(engine, nodeId)` draws every input pin of one node as a `ktsu.ImGui.Widgets.PropertyGrid`, disabled where a pin is connected or unsupported rather than omitted, so the panel never shows less than the node has. The renderer also reports `SelectedNodeIds`, read back from ImNodes the same way hover state is, which is what a host hands to `NodeInspectorPanel` for "inspect whatever is selected." A parameter has exactly one home, and `GetPinValue`/`SetPinValue`/`ResetPinValue` reach it whichever that is. `AttributeBasedNodeFactory.CreateNode` constructs the declared type once per node (recorded as a `NodeBinding(NodeId, Definition, Instance)`, read back with `GetBinding`, `GetNodeDefinition(int)` and `TryGetNodeInstance`) and registers a `PinValueAccessor(Func<object?> Get, Func<object?, bool> Set)` for each input pin backed by a writable property or field, so for those pins **the instance is the value** and an inline editor, an inspector row and `PinDefinition.GetValue` cannot disagree. Everything else falls back to `PinValueStore`: a method node (whose receiver arrives over its `Instance` input pin), a type with no parameterless constructor, a constructor or method parameter pin, an output pin, and any node built through the plain-name `CreateNode` overloads. `SetPinValue` type-checks against the pin's `DataType` first either way, so a refused value reaches neither home; `ResetPinValue` writes the seeded default back through the accessor; and `RemoveNode`/`Clear` unbind. See [Where a value lives](#editing-node-parameters) in the library's README. The engine learns nothing about reflection, `PinDefinition` or `ktsu.NodeGraph` — it holds delegates, and `BindPinValue`/`UnbindPinValue` are public, so a host with its own model can bind its own. One shared coercion (`PinValueStore.Coerce(Type?, object?)`) brings a declared default in line with the pin's type on both paths, so `[InputPin("X", DefaultValue = 50)]` on a `double` means `50.0` wherever it is read; a default that cannot convert at all leaves the member's own initializer standing.
- **ImGui.Markdown** (`ktsu.ImGui.Markdown`) - CommonMark markdown renderer built on Markdig (pipe tables, task lists, autolinks), layered on `ImGui.Color` only, with no dependency on `ImGui.App`. Static `ImGuiMarkdown.Render(string, MarkdownConfig?)` parses with an internal source-keyed cache; `MarkdownDocument` parses once for hot render paths. `MarkdownConfig` exposes `FontResolver`, `OnLinkClicked`, `ImageResolver`, `HeadingScales`, `WrapWidth`, `ListIndentPixels`, `ParagraphSpacingPixels`, and `LinkColor`. Heading sizes derive from the live font size, so DPI and `ImGuiApp.GlobalScale` are respected automatically. Bold/italic use real glyphs when the host app registers named font variants via `FontResolver`, otherwise faux styling (faux-bold double-draw, faux-italic renders upright). Fenced and indented code blocks go to `MarkdownConfig.CodeBlockRenderer` (`Action<string?, string>?` — the fence's info string and the block text) when one is supplied, which takes over drawing *and* reserving the block's layout space; `ImGui.SyntaxHighlighting` plugs into it, and neither library references the other. v1 has no built-in code-block syntax highlighting, no async remote image download, and renders HTML as escaped text.
- **SyntaxHighlighting** (`ktsu.SyntaxHighlighting`) - Renderer-agnostic tokenizing: no ImGui, no graphics API, no third-party parser, so it can move to its own repository unchanged. `SyntaxHighlighter.Highlight(code, language, tabWidth)` returns the classified `HighlightedLine`/`HighlightedToken` runs; `SyntaxHighlighter.HighlightCached` goes through a bounded cache keyed by source, language and tab width; `HighlightedCode` tokenizes once for hot render paths. Languages are data (`LanguageDefinition`: line/block comment, string, keyword, type, constant, operator, identifier and embedded-language rules) held in `LanguageRegistry`, which resolves names and aliases case-insensitively and falls back to plain text for unknown names rather than throwing. Fifteen built-ins in `BuiltInLanguages`: text, csharp, c, cpp, javascript, typescript, python, json, yaml, xml, html, css, sql, shell, lua. Two tokenizers back them — the general `CodeTokenizer`, and `MarkupTokenizer` for definitions with `IsMarkup` (XML/HTML), which classify structurally rather than by keyword. `SyntaxTheme` holds one `ktsu.Semantics.Color.Color` per `TokenKind`, with `Dark`/`Light` built in and `Background`/`Plain`/`LineNumber` left unset for the host to fill. Comments and strings are searched for an embedded language; see [Embedded languages](#embedded-languages) below. Highlighting is lexical.
- **ImGui.SyntaxHighlighting** (`ktsu.ImGui.SyntaxHighlighting`) - The Dear ImGui drawing layer over `ktsu.SyntaxHighlighting`, layered on `ImGui.Color` only, with no dependency on `ImGui.App`. Static `ImGuiSyntaxHighlighting.Render(code, language, SyntaxHighlightConfig?)` tokenizes through the shared cache and draws; `Render(HighlightedCode, config)` draws pre-tokenized code, and `HighlightedCodeExtensions` re-adds `code.Render(config)` as an extension since the tokenized type itself knows nothing about ImGui. `Highlight` forwards to `SyntaxHighlighter.Highlight`. Leaving `SyntaxHighlightConfig.Theme` null picks between `SyntaxTheme.Dark`/`Light` per frame from the window background's luminance, and unset `Background`/`Plain`/`LineNumber` come from `FrameBg`/`Text`/`TextDisabled`. Code is never wrapped, and there is no scrolling, selection or editing. `ImGui.Markdown`'s `CodeBlockRenderer` plugs into this, and neither library references the other.
Expand All @@ -60,7 +60,11 @@ This is the **ktsu ImGui Suite**, a collection of .NET libraries for building De
- `tests/ImGui.NodeEditor.Tests/` - Engine, factory and rendering tests for the node editor. The
engine and factory ones need no context; `NodeRenderingTests`, `ZoomTests` and
`HoverHighlightTests` drive real frames through `ImGuiAppHarness`, since zoom and hover are made
of what the renderer writes into ImNodes and reads back out, and neither exists without drawing
of what the renderer writes into ImNodes and reads back out, and neither exists without drawing.
`PinValueBindingTests` covers the seam between the two homes a parameter can have, and
`InlinePinEditorKindTests`/`NodeInspectorPanelKindTests` drive one test per `PinValueKind` on each
editing surface, which is what turns "a type has an editor on both surfaces or on neither" into
something the suite checks
- `tests/<Demo>.UITests/` - One headless UI test project per example, driving the demo's real
`BuildConfig()` through `ImGuiAppHarness`: `ImGuiAppDemo.UITests`, `ImGuiWidgetsDemo.UITests`,
`ImGuiStylerDemo.UITests`, `ImGuiPopupsDemo.UITests`, `ImGuiMarkdownDemo.UITests`,
Expand Down Expand Up @@ -135,7 +139,7 @@ This is the **ktsu ImGui Suite**, a collection of .NET libraries for building De
- **ktsu.Semantics.Strings** (1.0.28) - Type-safe string wrappers
- **ktsu.Semantics.Quantities** (1.0.29) - Typed quantity calculations
- **Hexa.NET.ImGui.Widgets** (1.2.18) - Upstream widget collection backing the Hexa-delegated widgets in `ImGui.Widgets`
- **Hexa.NET.ImGui.Widgets.Extras** (1.0.9) - Curve editor, bezier and text editor extras. Used by exactly two call sites, both under `ImGui.Widgets/Editors`: `CurveField.cs` (`ImGuiCurveEditor.Curve`) and `BezierEditor.cs` (`ImGuiBezierWidget.Bezier`). Pulls `Hexa.NET.Math` and `Microsoft.CodeAnalysis.CSharp.Scripting` into the dependency graph, and the latter brings the Roslyn compiler and scripting host with it — 5 packages, ~107 MB unpacked, which every consumer of `ktsu.ImGui.Widgets` restores and publishes whether or not it draws a curve. Roslyn is reachable only from Extras' `CSharpSyntaxHighlight`, a TextEditor type nothing here touches, so this is a restore and publish cost rather than a runtime one. Do not expect `ExcludeAssets`/`PrivateAssets` to remove it: the Roslyn dependency is declared in Extras' own nuspec, and a package cannot prune its dependency's dependencies for its consumers. Dropping the weight means splitting the two editors into an opt-in package or reimplementing them — tracked in #384.
- **Hexa.NET.ImGui.Widgets.Extras** (1.0.9) - Curve editor, bezier and text editor extras. Used by exactly two call sites, both under `ImGui.Widgets/Editors`: `CurveField.cs` (`ImGuiCurveEditor.Curve`) and `BezierEditor.cs` (`ImGuiBezierWidget.Bezier`). Pulls `Hexa.NET.Math` and `Microsoft.CodeAnalysis.CSharp.Scripting` into the dependency graph, and the latter brings the Roslyn compiler and scripting host with it — 5 packages, ~107 MB unpacked, which every consumer of `ktsu.ImGui.Widgets` restores and publishes whether or not it draws a curve. Roslyn is reachable only from Extras' `CSharpSyntaxHighlight`, a TextEditor type nothing here touches, so this is a restore and publish cost rather than a runtime one. Do not expect `ExcludeAssets`/`PrivateAssets` to remove it: the Roslyn dependency is declared in Extras' own nuspec, and a package cannot prune its dependency's dependencies for its consumers. Dropping the weight means splitting the two editors into an opt-in package or reimplementing them — tracked in #384. `ktsu.ImGui.NodeEditor` now carries this same weight at one remove: its inspector panel needs `PropertyGrid`, so `ImGui.NodeEditor.csproj` references `ImGui.Widgets`, which drags in `ImGui.Styler`, `ImGui.Color`, `ktsu.ThemeProvider`(+`.ImGui`) and this whole Extras/Roslyn graph behind it. Every consumer of `ktsu.ImGui.NodeEditor` restores and publishes it now too, whether or not they ever open the inspector.
- **ktsu.Invoker** (1.1.2) - Delegate invocation utilities
- **ktsu.ScopedAction** (1.1.6) - RAII-pattern scoped actions
- **Polyfill** (9.7.7) - Backport newer .NET APIs
Expand Down
Loading
Loading