feat(ui): Molten Works redesign (v2.0.0)#35
Merged
Wintersta7e merged 24 commits intomainfrom Apr 23, 2026
Merged
Conversation
- New theme palette (Mw tokens: iron, chrome, mercury, cyan, mag, lime, purple, molten tiers, bone, ash, faint) + typography (MwSerif/Display/ Mono/Body) in MoltenForgeTheme.axaml. Back-compat Forge* keys remapped. - Top bar rebuilt: FLOW·FORGE logo, molten works · v3 subtitle, pipeline path, STN/PIPE live stats, DRY RUN ghost + IGNITE molten / QUENCH cyan. - Operations library: // LIBRARY eyebrow, serif italic title, magenta search prompt, molten group eyebrows with fading rules, icon-well rows carrying category code (SHP.01). - Cast-iron stations: iron gradient body with 4 rivets, top code strip + LIVE/IDLE pill, 54×54 icon well, serif title + mono sub + config preview with category-neon left border, T/p/Δ gauge strip, chrome ports. - Chrome pipe connections (5-layer Nodify Connection stack). - Inspector: // INSPECTOR eyebrow, glowing icon-well header, empty prompt. - Bottom console: Cold / Forge Lit status, PID, molten progress bar with diagonal stripe overlay, throughput, 'The iron sings.' flavor. - Embedded fonts: Instrument Serif, Oswald, JetBrains Mono. - 11 forge-themed SVG icons (pit/mold/die/chisel/brand/sieve/rack/mill/ crucible/press/loupe) + Cog fallback. - MwOpsMap maps TypeKey → code/sub/category/icon; exposed via VM properties (MwCode, MwSub, MwCategory, MwIconGeometry, brushes). Co-Authored-By: Rooty
- Connection template: disabled Nodify's default arrowhead (ArrowEnds=None, ArrowSize=0), restyled with 5-layer chrome stroke + dark inner channel + molten gradient liquid + moltenHi bright core visible on running pipes. - Backdrop: radial forge glow, cyan/mag HUD washes, 40×40 minor + 200×200 major grid overlay via tiled DrawingBrush, magenta HUD corner brackets, system HUD (lat / mem / err) bottom-right. - MainWindowViewModel.UpdateRunningVisual propagates ExecutionLog.IsRunning → each PipelineNodeViewModel.IsRunning and PipelineConnectionViewModel.IsRunning. Stations and pipes drive their running visual state from this flag. - Icon well slightly narrowed, title 18→16, content margins tightened so longer station titles don't truncate. Co-Authored-By: Rooty
- PathGuard.EnsureWithinDirectory: normalize trailing directory separators (Path.TrimEndingDirectorySeparator) so a user-typed trailing slash doesn't produce a doubled separator and reject every valid child as 'outside source root'. FolderInputNode path enumeration gets the same fix. - Empty-value validation across node Configure methods — FolderInput, FolderOutput, RenamePattern, RenameRegex, ImageConvert — throw friendly NodeConfigurationException messages naming the station instead of letting raw ArgumentException / IO exceptions fan out per file. - ConfigFieldViewModel: normalize bool config values to 'True'/'False' (matches BoolStringConverter round-trip). Lowercase 'true'/'false' from JsonElement.GetRawText caused an infinite Fields-rebuild loop on every CheckBox toggle. - PropertiesView empty-state + fields ScrollViewer wrapped in a Grid so DockPanel default-Left docking doesn't squish the empty state to a narrow column when no node is selected. - OnUndoRedoStateChanged defers RefreshPropertiesPanel via Dispatcher.UIThread.Post so Fields don't rebuild inside a control's own event dispatch (would detach the live CheckBox mid-click and blank the inspector). - Regression tests covering each case (20 new tests). Co-Authored-By: Rooty
- Ember particles split into 14 per-index classes (mw-ember-0..13) each with unique Animation Delay and Duration so they permanently desync instead of rising in a single lockstep row. - DEMO button in the top bar toggles IsDemoMode on MainWindowViewModel. UpdateRunningVisual OR's ExecutionLog.IsRunning with IsDemoMode and pushes to stations + connections — exercises all running-state animations without needing a real pipeline run. Disabled while a real pipeline is running so the two states don't conflict. Co-Authored-By: Rooty
- MwPipeConnection: custom Shape that replaces Nodify's Connection in the pipe template. Renders the concept's simple midpoint cubic (M sx sy C mx sy, mx ty, tx ty) so we own the exact curve. Stacked 5 times in the ConnectionTemplate for shadow / chrome outer / chrome core / inner channel / liquid layers. - MwMercuryDroplet: Control that positions itself via Canvas.SetLeft/Top in MeasureOverride and paints one cached RadialGradientBrush ellipse (mercury core → moltenHi → molten halo → transparent rim). Shares the midpoint-cubic math with MwPipeConnection so the bead tracks the rendered pipe exactly. - Four droplets per pipe with staggered Delay (0 / 0.6 / 1.2 / 1.8s) over a 2.4s infinite loop, visible only on running pipes. - Pulsing LIVE dot on each station (1s opacity cycle + DropShadow). - Input/output port dots swap from chrome-dark to mercury with a moltenHi DropShadowEffect halo when the node runs. - PipelineNodeViewModel now exposes CategoryGlowColor (bindable Color for DropShadowEffect) and MwHeatPulseBrush (category-tinted radial). Previous attempts replicated Nodify's offset/distance-scaled bezier in the droplet math; fragile and error-prone. Owning the pipe lets droplets match by construction. Co-Authored-By: Rooty
- Iron body carries a DropShadowEffect with Color bound to CategoryGlowColor; Opacity animates 0.75→1→0.75 over 1.8s so running stations visibly breathe in their category color (molten for source/ heat, cyan for sinks, magenta for shape ops, lime for filters, purple for metadata). - Icon well gains its own DropShadow halo when running. - Heat pulse inside the iron: RadialGradientBrush tinted per category, BlurEffect Radius animates 10→14px alongside Opacity breathe — matches the concept's mwHeatPulse keyframes. - Gauge strip (T/p/Δ) animates per row: opacity 0.5 idle → 1.0 running, value color swaps IronLite dim → category neon bright. - Station sizes match concept spec (54×54 icon well, 18pt title, 30×30 icon viewbox, inset 28/38/28/14 main-body margins). - ClipToBounds=False on the nodify|ItemContainer style so the station's halo escapes the 200×130 container cell and actually reads as a halo around the node. Previous iterations tried filled-Border halos behind the iron (rendered as solid colored rectangles, not shape-projected shadows). The final approach keeps the halo as a blurred copy of the iron body's rounded- rect shape. Co-Authored-By: Rooty
- Drop the old editor-overview, node-library, node-pipeline and properties-panel shots (pre-redesign UI). - New screenshots: editor-overview (full app), stations-running (close-up of glowing cast-iron stations with mercury in the pipes), empty-state (hero page with the "Build your pipeline from raw ore" headline and template cards). - README rearranged so the three shots sit in logical sections and no longer appear back-to-back: empty-state at the top hero, editor-overview under the Pipeline Editor heading, stations-running after the editor bullet list supporting the Molten Works theme bullet. - Dropped the "Light/Dark Theme" feature bullets ahead of the removal commit that follows. Co-Authored-By: Rooty
Molten Works is inherently dark (cast-iron + molten). The Light ResourceDictionary was never designed faithfully, it was a rough approximation, and the toggle button stopped making sense after the redesign — so remove it entirely. - MoltenForgeTheme.axaml: delete the Light ThemeDictionary block (~180 lines of placeholder light values). - MainWindowViewModel: remove ToggleTheme command, IsDarkTheme, ThemeIcon and the unused `using Avalonia;` import. - ToolbarView: remove the theme-toggle icon button. - PipelineNodeViewModel: remove the ActualThemeVariantChanged subscription and the _themeChangedHandler field; drop Detach() since it was only there to unsubscribe. EditorViewModel call sites updated. - NodeLibraryViewModel: remove now-unused RefreshBrushes helper. - MwMercuryDroplet: rename _dropletBrush → DropletBrush to match the PascalCase-for-static-readonly .editorconfig rule. - App.axaml keeps RequestedThemeVariant="Dark" — the explicit lock. 368 tests pass. dotnet format clean. Co-Authored-By: Rooty
Captures the work on feature/molten-works-redesign: the redesign itself, the core validation fixes, and the dark-only theme decision. Once the branch is reviewed and ready to release it gets renamed from Unreleased to the chosen version. Co-Authored-By: Rooty
ExecutionLogViewModel.Progress is computed as percentage (0..100) and set to 100.0 on completion, but the ProgressBar had Maximum=1, so the bar clamped to full after ~1% progress on every real run. Co-Authored-By: Rooty
- Move Canvas.SetLeft/Top from MeasureOverride to ArrangeOverride: measure is pure (constant 22x22), arrange owns the animated position. Switch the invalidation hooks from AffectsMeasure+AffectsRender to AffectsArrange only, since render output does not depend on Source/Target/Progress. - Clamp Progress to [0, 1] in ComputeBezierPoint so keyframe overshoot at animation boundaries cannot send the bead off the pipe. - Rewrite the class summary to describe the actual single-ellipse radial-gradient render (it previously described three stacked circles that the type no longer paints) and drop the unused MiddleRadius / CoreRadius constants. Co-Authored-By: Rooty
- Guard UpdateRunningVisual with Dispatcher.UIThread.CheckAccess and re-post to the UI thread when called off-thread. The handler is reached from ExecutionLog.IsRunning flipping in the finally of an async pipeline, whose continuation can resume on a thread-pool thread; the previous code mutated Editor.Nodes / Editor.Connections (both ObservableCollection) from whatever thread finished RunAsync. - Marshal the finally-block ExecutionLog.IsRunning = false through Dispatcher.UIThread.Post so the setter's PropertyChanged cascade (including XAML bindings) stays on the UI thread. - Subscribe to Editor.Nodes.CollectionChanged and Editor.Connections.CollectionChanged; when a new station or pipe is added while the canvas is already in running/demo state, seed IsRunning on the new item so it matches the surrounding animation instead of staying visually idle. Co-Authored-By: Rooty
PathGuard + FolderInputNode:
Path.TrimEndingDirectorySeparator preserves the separator at the root of
a path (drive roots 'C:\' and UNC shares '\\server\share\'). Appending
DirectorySeparatorChar then produced a doubled separator ('C:\\') and
the StartsWith prefix check rejected every valid child when the root
itself was selected as source/output. Extracted NormalizedRootPrefix
helper that returns resolvedRoot unchanged if it already ends with the
separator, otherwise appends one, and route both PathGuard and
FolderInputNode through it.
The RenamePatternNode traversal test previously relied on the bug: both
the doctored CurrentPath and its derived 'directory' resolved to the
filesystem root, so the doubled-separator prefix check produced the
expected throw by accident. With the prefix correct, that scenario is a
no-op (rename stays in the resolved directory). Rewritten to use a
crafted rename pattern containing '..' — the real traversal vector the
guard is meant to catch.
ConfigFieldViewModel:
OnValueChanged had an early guard for unparseable Int input but not for
Bool: a non-'True'/'False' string fell through to the raw-string arm and
was serialised into a Bool-typed JSON slot, which would crash GetBoolean
on the next load. Added the symmetric bool.TryParse guard.
Co-Authored-By: Rooty
Close three load-bearing gaps the branch had left uncovered:
- MwOpsMapTests: parametric assertion that every TypeKey in the default
NodeRegistry has a bespoke MwOpsMap entry (non-Cog icon, non-empty
sub/code). A rename in NodeRegistry would otherwise silently fall
through to the generic fallback, landing the node in the wrong aura /
category with no runtime error.
- MainWindowViewModelTests: cover UpdateRunningVisual propagation for
ExecutionLog.IsRunning, the IsDemoMode OR path, and the
CollectionChanged seeding for stations and pipes added while the
canvas is already lit. Added InternalsVisibleTo("FlowForge.Tests") on
FlowForge.UI so the test can reach MwOpsMap (internal).
- ConfigFieldViewModelTests: add explicit regression tests for the
original infinite-loop premise (setting Value to the current value
must not fire PropertyChanged) and for the Bool-branch guard (an
unparseable bool string must not overwrite the stored JsonElement).
Test count 368 -> 375, all green.
Co-Authored-By: Rooty
Comments on source should say *why* the code is shaped the way it is, not retell the bug that motivated each change — that story belongs in git log. Rewrote the trailing-separator and bool-normalization narratives in PathGuard (earlier commit), FolderInputNode, the two regression tests, and ConfigFieldViewModel to present-tense invariant descriptions. Dropped the three one-line "wire X to Y" comments above MainWindowViewModel's handler subscriptions — the handler names already say that. Co-Authored-By: Rooty
The existing "bespoke entry" test only rejected the Cog fallback and empty sub/code strings — it did not catch a source or output node that was entered into the map but placed in the wrong category bucket (e.g. "hea" for a source node). That produces a silent visual regression: correct icon and subtitle, wrong aura / port glow / neon accent colour. Added a second assertion that walks NodeRegistry.GetCategoryForTypeKey and requires the MwOpsMap bucket to match: Source → "src", Output → "snk", Transform → one of "shp"/"flt"/"hea"/"met". Co-Authored-By: Rooty
OnEditorRunningCollectionChanged previously bailed on args.NewItems == null, which skipped the Reset action (bulk Clear+Add). A template swap or ClearAll-then-Load mid-run would leave the new canvas dark while ExecutionLog.IsRunning / IsDemoMode was still true. On Reset, delegate to UpdateRunningVisual to re-sync the whole canvas. ExecutePipelineAsync's finally used fire-and-forget Dispatcher.Post for the IsRunning = false flip. Async Post leaves a race window: a second invocation reaching the "if (IsRunning) return" guard before the posted lambda runs would see the previous run as still-running, skip the new run, and silently drop the user's click. Switched to awaited Dispatcher.UIThread.InvokeAsync so the next ExecutePipelineAsync call is guaranteed to see IsRunning = false. Co-Authored-By: Rooty
Math.Clamp(NaN, 0, 1) returns NaN — without a boundary check, a NaN or infinite value written to ProgressProperty by a misconfigured upstream animator would propagate through ComputeBezierPoint into Canvas.SetLeft/Top and corrupt the bead's rendered position. Same exposure on Source/Target if either endpoint is ever non-finite. Added an IsFinite validate callback on all three StyledProperty registrations so Avalonia rejects the bad value at SetValue rather than letting it reach the layout/render code. Co-Authored-By: Rooty
Trim speculative / bug-narrative tails from four test XMLdocs that R1
had rewritten once but still retained flavour text ("silently dead-ink
the canvas", "Regression guard for the original infinite-loop
scenario", "nothing would crash to flag the gap", …). Each xmldoc is
now a present-tense invariant matching what the test name already
implies.
Delete two carry-over inline comments that only restated the next
line: "// Capture old value before mutation" above TryGetValue, and
"// Show the first string config value as a preview" above the foreach
in BuildConfigPreview.
Co-Authored-By: Rooty
MwMercuryDroplet already rejected non-finite Source/Target/Progress via StyledProperty validate callbacks, but MwPipeConnection did not. The two types share the same cubic midpoint formula by construction — they should also share the same boundary contract so a NaN endpoint can't corrupt the pipe StreamGeometry while the bead above it refuses to render. Extracted the finite-check into a new internal static MwGeometry with double / Point / Size overloads and delegated the double path to the BCL double.IsFinite. MwPipeConnection now validates Source, Target, SourceOffset, and TargetOffset; MwMercuryDroplet's private helpers are replaced with MwGeometry calls. Co-Authored-By: Rooty
- Int_field_rejects_unparseable_input_without_mutating_config: mirror of the Bool guard test. The Int rejection branch in OnValueChanged has been in the file since v1.1 but only the happy path was covered. Adds the symmetric rejection assertion so the guard can't be silently removed later. - Nodes_added_after_collection_reset_during_demo_are_seeded: covers the OnEditorRunningCollectionChanged Reset branch added in the previous fix commit. Clear() + Add() in demo mode previously would have left the new node idle; this test pins the seed-on-Reset behaviour so the gap can't reopen. Test count 376 -> 378. Co-Authored-By: Rooty
Version-bump Directory.Build.props to 2.0.0. Finalize CHANGELOG: [Unreleased] → [2.0.0] - 2026-04-23 with the full set of adds, changes, and fixes from the Molten Works branch + the four rounds of review-driven hardening (progress-bar scale, droplet arrange + progress clamp + finite-validator, running-state thread safety + CollectionChanged seeding incl. Reset, PathGuard root-prefix helper, symmetric bool/int unparseable-input guards, shared MwGeometry validator for pipe/bead, 30+ new regression tests — 378 total). Major version bump because the light theme was removed on this branch; anyone who was actively using it will need to rebase. Co-Authored-By: Rooty
Button copy in the Running Pipelines section was still "Preview / Run / Cancel" while the actual Molten Works toolbar now reads "Dry Run / Ignite / Quench". Update the three subsection headers and their body text, add a short Demo subsection covering the toolbar toggle that exercises running visuals without a real pipeline. Co-Authored-By: Rooty
The three embedded font families (Instrument Serif, Oswald, JetBrains Mono) are each licensed under the SIL Open Font License 1.1, which requires the licence text to travel with the redistributed font binaries — in both source and binary distribution. The repo and the release zip were previously shipping the TTFs without the OFL text. Changes: - Add OFL.txt alongside the fonts with per-font copyright lines and the full OFL 1.1 text. - Wire it as Content with CopyToOutputDirectory=PreserveNewest on FlowForge.UI.csproj so it lands next to the bundle in every publish. - Add THIRD-PARTY-NOTICES.md at repo root enumerating the fonts + their upstream URLs + licence, plus the notable NuGet dependencies. - Reference THIRD-PARTY-NOTICES.md from README's Licence section. - Release workflow copies LICENSE (renamed to LICENSE.txt for Windows double-click) and THIRD-PARTY-NOTICES.md into publish/ before zipping so both licence documents ship with the downloadable release. Co-Authored-By: Rooty
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MwPipeConnection : ShapeandMwMercuryDroplet : Controlshare the same midpoint-cubic formula so droplets track the rendered pipe by construction; a sharedMwGeometry.IsFinitevalidator rejects NaN / ±Infinity at the StyledProperty boundary on both sides of the curve.App.axamllocksRequestedThemeVariant="Dark"; the runtime toggle,IsDarkTheme/ThemeIconproperties, and theActualThemeVariantChangedsubscription are removed.ArrangeOverride+ Progress clamp, running-state thread safety (Dispatcher.UIThread.InvokeAsyncin the finally,CollectionChangedseeding on Add + Reset),PathGuarddrive-root / UNC prefix helper, symmetricbool/intunparseable-input guards inConfigFieldViewModel.OFL.txt,THIRD-PARTY-NOTICES.md, andLICENSE.txtin every release zip (the three embedded font families were previously redistributed without their licence text).dotnet formatclean. 24 commits, 2.0.0 finalized inDirectory.Build.props+ CHANGELOG.See CHANGELOG.md for the full change list.