Skip to content

Give the Streckenplaner real track CRUD and a self-fitting layout - #13

Merged
Ktechen merged 1 commit into
masterfrom
feat/track-planner-crud-and-layout
Aug 19, 2026
Merged

Give the Streckenplaner real track CRUD and a self-fitting layout#13
Ktechen merged 1 commit into
masterfrom
feat/track-planner-crud-and-layout

Conversation

@Ktechen

@Ktechen Ktechen commented Aug 19, 2026

Copy link
Copy Markdown
Owner

The 2D planner was a single hard-coded 8-segment stadium loop. It now builds arbitrary plans: append Gerade/Kurve/Weiche/Bahnhof, duplicate, reorder, delete, or start from an Oval/Acht/Punkt-zu-Punkt template, with a Weiche splitting the plan into a Stammgleis and its own branch strand that can be built on further.

Core:

  • Domain gains CurveDirection/SwitchRoute and the strand/branch fields on TrackSegment; Application gains the pure TrackPlanService (append/insert-switch/ duplicate/reorder/delete/templates) behind ITrackPlanService, TrackSegmentDto + mapping, and ITrackSegmentRepository; Infrastructure persists segments via SqliteTrackSegmentRepository on the existing BaseRepository.
  • Deleting a Weiche cascades through its branch, including nested Weichen.

Layout (Trackify/Helpers/TrackGeometry.cs) is now turtle graphics over the strand graph instead of a closed form, in two passes: the walk lays every strand out in unbounded world coordinates, then the emit pass bakes in the scale/offset that centers the finished plan on the 900x600 canvas and shrinks it (never enlarges) when it would reach into the margin the labels and sensor markers need. A plan can therefore grow in any direction without walking off the visible area — before, the walk started at a fixed (150,300) and the Oval sat in the left third.

Position fixes that came out of that:

  • A curve's outward normal is derived from the tangent, like a straight's, instead of "away from the arc center" — the latter flips between left and right curves, so speed labels and sensor markers changed sides mid-plan.
  • The direction arrow is emitted as path data (TrackSegment.ArrowPathData) rather than a triangle rotated by a bound RenderTransform: a {Binding} inside a transform has no DataContext to inherit — transforms aren't in the visual tree — so the angle never arrived, and the old geometry's negative coordinates left the arrow's origin up to the renderer.
  • The Acht template's second lobe leads with its turn. Leading with straights, as the first lobe does, retraced its opening Gerade and Bahnhof piece-for-piece from the same anchor pose: two segments stacked at identical coordinates.
  • A strand nothing branches into (a branch whose Weiche is gone, e.g. from a plan saved mid-write) is parked on its own lane below the plan instead of silently keeping the position it had in the previous layout.
  • ApplyPlan computes the layout once instead of twice (segments + ballast path).

UI: the planner page is a splitter-resizable canvas / strand list / segment inspector, collapsing to a stacked layout with the inspector as a bottom sheet on narrow screens; zoom is a bound ZoomFactor driving a Viewbox, not a ScrollViewer call. New StrandList and BlueprintFrame, Column/RowSplitterBehavior, and refreshed design tokens and palette.

Verified: all four buildable Uno heads (desktop/windows/android/browserwasm), CLI, dotnet test (40 passing, incl. new TrackPlanServiceTests), desktop launch smoke test. The canvas itself cannot be pixel-verified in this environment, so the visuals still need a look on a real device.

The 2D planner was a single hard-coded 8-segment stadium loop. It now builds
arbitrary plans: append Gerade/Kurve/Weiche/Bahnhof, duplicate, reorder, delete,
or start from an Oval/Acht/Punkt-zu-Punkt template, with a Weiche splitting the
plan into a Stammgleis and its own branch strand that can be built on further.

Core:
- Domain gains CurveDirection/SwitchRoute and the strand/branch fields on
  TrackSegment; Application gains the pure TrackPlanService (append/insert-switch/
  duplicate/reorder/delete/templates) behind ITrackPlanService, TrackSegmentDto +
  mapping, and ITrackSegmentRepository; Infrastructure persists segments via
  SqliteTrackSegmentRepository on the existing BaseRepository.
- Deleting a Weiche cascades through its branch, including nested Weichen.

Layout (Trackify/Helpers/TrackGeometry.cs) is now turtle graphics over the strand
graph instead of a closed form, in two passes: the walk lays every strand out in
unbounded world coordinates, then the emit pass bakes in the scale/offset that
centers the finished plan on the 900x600 canvas and shrinks it (never enlarges)
when it would reach into the margin the labels and sensor markers need. A plan can
therefore grow in any direction without walking off the visible area — before,
the walk started at a fixed (150,300) and the Oval sat in the left third.

Position fixes that came out of that:
- A curve's outward normal is derived from the tangent, like a straight's, instead
  of "away from the arc center" — the latter flips between left and right curves,
  so speed labels and sensor markers changed sides mid-plan.
- The direction arrow is emitted as path data (TrackSegment.ArrowPathData) rather
  than a triangle rotated by a bound RenderTransform: a {Binding} inside a
  transform has no DataContext to inherit — transforms aren't in the visual tree —
  so the angle never arrived, and the old geometry's negative coordinates left the
  arrow's origin up to the renderer.
- The Acht template's second lobe leads with its turn. Leading with straights, as
  the first lobe does, retraced its opening Gerade and Bahnhof piece-for-piece from
  the same anchor pose: two segments stacked at identical coordinates.
- A strand nothing branches into (a branch whose Weiche is gone, e.g. from a plan
  saved mid-write) is parked on its own lane below the plan instead of silently
  keeping the position it had in the previous layout.
- ApplyPlan computes the layout once instead of twice (segments + ballast path).

UI: the planner page is a splitter-resizable canvas / strand list / segment
inspector, collapsing to a stacked layout with the inspector as a bottom sheet on
narrow screens; zoom is a bound ZoomFactor driving a Viewbox, not a ScrollViewer
call. New StrandList and BlueprintFrame, Column/RowSplitterBehavior, and refreshed
design tokens and palette.

Verified: all four buildable Uno heads (desktop/windows/android/browserwasm), CLI,
dotnet test (40 passing, incl. new TrackPlanServiceTests), desktop launch smoke
test. The canvas itself cannot be pixel-verified in this environment, so the
visuals still need a look on a real device.

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
8.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Ktechen
Ktechen merged commit ca15b56 into master Aug 19, 2026
5 of 6 checks passed
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.

1 participant