Skip to content

Replace v8 bindings with v9 profile-wide node graph#34

Open
Zanges wants to merge 4 commits into
mainfrom
feature/node-based-profiles
Open

Replace v8 bindings with v9 profile-wide node graph#34
Zanges wants to merge 4 commits into
mainfrom
feature/node-based-profiles

Conversation

@Zanges
Copy link
Copy Markdown
Owner

@Zanges Zanges commented May 25, 2026

Summary

  • Replaces the v8 Binding+Modifier model with a single profile-wide node graph (shader-editor style). New Mouse2Joy.Graph project holds 54 typed node kinds, GraphCompiler, and GraphEvaluator. Engine routes through GraphRunner; persistence drops the migration chain and FirstRunBackup moves pre-v9 profiles aside on first launch.
  • UI ships a hand-rolled Canvas-based graph editor: draggable node cards with inline parameter editors, port-to-port wire dragging with auto-insert converter pickers, side variables panel, marquee/group/copy-paste, undo/redo with drag and idle coalescing.
  • 147 tests pass (Graph 21, Persistence 10, Engine 18, Contracts 61, Platform.Abstractions 5, UI 32). See ai-docs/implementations/NODE_BASED_PROFILES.md for the full design write-up, key decisions, and deferred follow-ups.

Test plan

  • dotnet build Mouse2Joy.sln succeeds.
  • dotnet test Mouse2Joy.sln passes all 147 tests.
  • Launch elevated: dotnet run --project src/Mouse2Joy.App. On first launch with pre-v9 profiles in %APPDATA%\Mouse2Joy\profiles\, confirm the one-time modal lists moved files and the folder is repopulated with a fresh empty state.
  • Create a new profile, click Edit graph…, drop a Mouse → Vector2StickDynamics → LeftStickVec graph, click Save, click Activate, confirm the virtual pad's left stick responds to mouse motion.
  • Test live editing: open the editor again on the active profile, change a parameter (e.g. StickDynamics Mode), click Save, confirm the engine picks up the new graph without a profile re-activate.
  • Test the converter pickers: drop a Key → Compare edge (expects Digital→Scalar picker), drop a Mouse.Motion → LeftStickX edge (expects Vector2→Scalar picker), drop a Mouse.Motion → Button edge (expects rejection toast).
  • Test variables: add a variable in the side panel, drop Get/Set nodes for it, rename it, confirm both nodes rewire automatically.
  • Test undo/redo: make several edits including a drag and a parameter change; confirm Ctrl+Z collapses each gesture into one entry.

🤖 Generated with Claude Code

Replaces the v8 Binding+Modifier model with a single profile-wide node
graph (shader-editor style). New Mouse2Joy.Graph project holds 54 typed
node kinds across Sources / Targets / Math / Logic / Conversion / Timing
/ Curves / Dynamics / Limits / Variables, plus GraphCompiler and
GraphEvaluator. Engine routes through GraphRunner; persistence drops the
migration chain and FirstRunBackup moves pre-v9 profiles aside.

UI ships a hand-rolled Canvas-based graph editor: draggable node cards
with inline parameter editors, port-to-port wire dragging with auto-
insert converter pickers, side variables panel, marquee/group/copy-paste,
undo-redo with drag and idle coalescing. See
ai-docs/implementations/NODE_BASED_PROFILES.md for the full design write-
up and the deferred follow-ups.

147 tests pass (Graph 21, Persistence 10, Engine 18, Contracts 61,
Platform.Abstractions 5, UI 32).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 22:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@github-actions github-actions Bot added tests Test-only changes docs Documentation-only changes ui Changes to Mouse2Joy.UI engine Changes to Mouse2Joy.Engine persistence Changes to Mouse2Joy.Persistence app Changes to Mouse2Joy.App build Build, packaging, installer changes labels May 25, 2026
Zanges and others added 2 commits May 26, 2026 00:20
CI's Build & Test step runs `dotnet format --verify-no-changes` and
flagged whitespace in GraphRunner.cs, ConversionEvaluators.cs,
SourceEvaluators.cs, and UndoStackTests.cs. Running `dotnet format`
locally also normalized line endings across the new Graph project
files. No behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses 8 of 10 findings from the multi-angle code review on PR #34.
The two unfixed findings (settings v1→v2 migration drop and global
KeyDown intercepting TextBox edits) are documented as follow-ups.

- Variable-slot staleness (A): GraphCompiler no longer reuses
  Get/SetVariable evaluators across recompiles. Their cached
  _variableSlot index was stale if another variable was deleted
  between compiles, causing IndexOutOfRange on the next tick.
- Plan-swap and suppression-set sync (B, C): added _planLock around
  GraphEvaluator's plan + slot arrays + variable buffers, and a
  _suppressionLock around GraphRunner's _suppressedKeys / _suppressMouse.
  Both prevent capture-thread vs tick-thread races when a profile-switch
  hotkey fires.
- Multi-edge into single target-port (F): GraphValidator no longer
  exempts target nodes from the "one inbound edge per input port" rule.
  The exemption was meant for multiple node *instances*, not multi-edge
  into one node. Multi-edges into the same port were silently dropped in
  the compiler — now they're rejected at validation.
- OnEditGraph rollback no-op (D): MainWindow now captures the original
  profile reference before overwriting _vm.SelectedProfile, so save-
  failure rollback actually reverts.
- FirstRunBackup hardening (G, I, L): backup directory + per-file
  destination paths get auto-suffixed on collision (same-second re-runs
  no longer leave legacy files in profiles/); PeekSchemaVersion catches
  all IO/access exceptions (UnauthorizedAccessException, locked cloud
  stubs, etc.) and falls back to "leave in place"; non-Int32
  schemaVersion is treated as legacy.
- Typed slot initials (J): GraphCompiler now threads its per-port-type
  slotInitials through CompiledGraph.SlotInitials → GraphEvaluator. A
  first-tick read of an unconnected non-Scalar input port now returns
  a type-correct Signal.Zero (Vector2/Digital/Delta) instead of a
  Scalar-tagged zero.
- Compile-error guard on Off→Active (K): RequestToggle's Off-side
  branches (both Soft and Hard) now check HasCompileError, matching the
  existing SoftMuted→Active guard. Engine no longer silently enters
  Active mode with an empty compiled plan.
- Defensive ProfileStore schema check: DeserializeProfile now rejects
  any profile whose deserialized SchemaVersion is below current. Belt
  and suspenders for FirstRunBackup — if a legacy file slips past
  (e.g. file locked during the backup scan), the user just doesn't see
  it in the profile list rather than getting an empty-graph profile
  that overwrites the legacy bindings on next save.

Adds 6 regression tests across Graph and Persistence test projects.
153 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 23:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Implements all critical findings from the local 8-angle review and the
cheap subset of suggestions. Deferred items live in
ai-docs/implementations/NODE_BASED_PROFILES_FOLLOWUPS.md with rationale
and concrete next steps for each.

Correctness (6 critical NaN/overflow fixes):
- MathEvaluators.DivideEvaluator: guard long.MinValue / -1L → MaxValue
- MathEvaluators.NegateEvaluator: guard -long.MinValue → long.MaxValue
- DynamicsEvaluators.StickDynamicsEvaluator: NaN sanitization on
  Param1/Param2 and a belt-and-suspenders NaN reset on _deflection
- DynamicsEvaluators.Vector2StickDynamicsEvaluator: same for the joint
  integrator state
- CurveEvaluators.ResponseCurveEvaluator: IsNaN || <=0 exponent guard
- CurveEvaluators.SegmentedResponseCurveEvaluator: same plus NaN
  threshold fallback to 0.5

Also: MathEvaluators.CompareEvaluator integer mode catches
NaN/Infinity threshold (would silently produce wrong booleans);
ConversionEvaluators.ScalarToIntEvaluator explicitly handles
NaN→0 / ±Infinity→long extremes.

UX (4 critical drop-position / variable-name / rename fixes):
- GraphCanvasControl.ScreenCenterInCanvasCoords now reads the actual
  ScrollViewer offset + viewport (no more hardcoded 500,300)
- New NextDropPosition() fan-offset prevents successive Add-Node /
  Drop-Get / Drop-Set from stacking on top of each other
- VariablesPanel "Drop Get" / "Drop Set" use the canvas's
  NextDropPosition() instead of hardcoded (600,400) / (600,460)
- NodeParameterEditors variable-name field replaced with a ComboBox
  bound to the declared variables (with a stale-name placeholder for
  references to deleted variables)
- VariablesPanel.Rename surfaces inline errors (empty name, duplicate)
  and reverts the TextBox instead of silently swallowing

Plus: explicit Name label on variable rows; "Default value" rather
than "Default true"; "Rounding" label on ScalarToInt; Persistent-mode
StickDynamics hides the unused Param2; node card × button gains a
tooltip; toolbar Save/Delete tooltips mention Ctrl+S / Del;
NodePicker auto-selects first row on filter so Enter always commits;
NodePicker filter searches port-type names too; PasteVariablesDialog
shows explanatory text instead of a disabled ComboBox when
"Keep reference" isn't a real choice; DoubleBox/IntBox/LongBox commit
on Enter as well as LostFocus and revert on parse failure;
MainWindow "(no profile selected)" → actionable "Select a profile…".

Convention (5 criticals):
- Removed the "good enough for v1" shortcut on ScreenCenter
- New per-kind evaluator test files: CurveEvaluatorTests,
  DynamicsEvaluatorTests, MathEvaluatorTests (18 new tests covering
  identity defaults, sign symmetry, NaN guards, overflow guards)
- Settings v1→v2 migration drop: documented as a conscious convention
  break in NODE_BASED_PROFILES.md (Key decisions), trade-off recorded
- NodeEditorAvalonia 11.3.11 dropped (unused package was shipping in
  the admin-process app output)

Performance (critical):
- EvaluationContext exposes ReadInputBySlot / WriteOutputBySlot direct
  accessors; PortSlotMap made public so evaluators can cache slot ids
- Hot-path evaluators migrated: MouseEvaluator (8 ports/tick),
  KeyEvaluator, StickDynamicsEvaluator, Vector2StickDynamicsEvaluator.
  Remaining evaluators tracked as a follow-up.

Stability:
- NodeCard.PropertyChanged handler hoisted into a named method and
  unsubscribed on DetachedFromVisualTree (fixes the per-edit memory
  growth proportional to edits × node count per editor session)
- InputSnapshot now documents the single-threaded invariant so future
  refactors don't reintroduce direct cross-thread mutation

Regression / docs:
- INITIALWORK.md banner updated to point at NODE_BASED_PROFILES.md
  (was still describing the deleted v8 binding-editor architecture)
- NODE_BASED_PROFILES_FOLLOWUPS.md tracks 11 deferred items with
  rationale and next steps

171 tests pass (was 153, +18 evaluator regression tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Changes to Mouse2Joy.App build Build, packaging, installer changes docs Documentation-only changes engine Changes to Mouse2Joy.Engine persistence Changes to Mouse2Joy.Persistence tests Test-only changes ui Changes to Mouse2Joy.UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants