diff --git a/program/MainWindow.cs b/program/MainWindow.cs index 767e890..65e15cc 100644 --- a/program/MainWindow.cs +++ b/program/MainWindow.cs @@ -126,7 +126,7 @@ _settingsWindow = new SettingsWindow(builder, _arduino, _Language); _settingsWindow.BackRequested += OnSettingsClosed; - _mapWindow = new MapWindow(builder, _Language); + _mapWindow = new MapWindow(builder, _arduino, _Language); _mapWindow.BackRequested += OnMapClosed; _Language.Changed += (s, e) => OnArduinoConnectionChanged(_connected); diff --git a/program/MapWindow.cs b/program/MapWindow.cs index 805a888..8a0a158 100644 --- a/program/MapWindow.cs +++ b/program/MapWindow.cs @@ -9,6 +9,12 @@ public class MapWindow : IDisposable { + private const float FRONT_Z = 1.0f; + private const float BACK_Z = -1.0f; + private const float RIGHT_Z = 0.5f; + private const float LEFT_Z = -0.5f; + private const float Z_TOLERANCE = 0.1f; + // ══════════════════════════════════════════════════════════════════ // TUNABLES — per (jacket size, view). Absolute values, subtracted. // Paste a CalibrationWindow dump into the tuner to overwrite these, @@ -27,8 +33,12 @@ public double MapAlpha = 0.7; public double FitMargin = 40.0; // Form2: panel − 40 (front: −100) // private MapFit _mapFit = MapFit.FitBody; + // cursor calibration + public double MarginX = 0.0; + public double MarginY = 0.0; public Tune Clone() { return (Tune)MemberwiseClone(); } + } /// How the MAP is sized over the body. @@ -105,7 +115,18 @@ private Gdk.Pixbuf _mapRaw; // keyed, ORIGINAL size — used by FitBody mode private CaseDefinition _case; private string _view = "front"; + private readonly ArduinoConnection _arduino; + // ── live Arduino position ── + private float _currentX, _currentY, _currentZ; + private bool _hasPosition; + + private static readonly Regex FullRx = new Regex( + @"(\d+(?:\.\d+)?)\(([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?)\)", + RegexOptions.Compiled); + private static readonly Regex AvgRx = new Regex( + @"Avg(\d+(?:\.\d+)?)\(([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?)\)", + RegexOptions.Compiled); private string _bodyPath = "(none)"; private string _mapPath = "(none)"; @@ -133,9 +154,11 @@ // ══════════════════════════════════════════════════════════════════ - public MapWindow(Builder builder, Language language) + public MapWindow(Builder builder, ArduinoConnection arduino, Language language) { builder.Autoconnect(this); + _arduino = arduino; + SeedDefaults(); LoadCalibFile(false); // calib.txt overrides seeds if present @@ -176,6 +199,9 @@ }; _sound.Log += msg => Logger.Write("sound", msg); + // NEW — live position feed + _arduino.LineReceived += OnArduinoLine; + _arduino.ConnectionChanged += OnArduinoConnectionChanged; } private void HookKeys() @@ -199,6 +225,148 @@ rb.Toggled += (s, e) => { if (rb.Active) SetView(view); }; } + // ══════════════════════════════════════════════════════════════════ + // ARDUINO POSITION + // ══════════════════════════════════════════════════════════════════ + + private void OnArduinoLine(string line) + { + var avg = AvgRx.Match(line); + float x, y, z; + + if (avg.Success) + { + x = ParseF(avg.Groups[1].Value); + y = ParseF(avg.Groups[2].Value); + z = ParseF(avg.Groups[3].Value); + } + else + { + var tag = TagRx.Match(line); + if (!tag.Success) return; // status/mode line — not a coordinate + x = ParseF(tag.Groups[2].Value); // group 1 is the tag number itself now + y = ParseF(tag.Groups[3].Value); + z = ParseF(tag.Groups[4].Value); + } + + _currentX = x; + _currentY = y; + _currentZ = z; + _hasPosition = true; + + string newView = DetermineViewFromZ(z); + if (newView != _view) + SwitchViewFromArduino(newView); + + MapArea.QueueDraw(); + Logger.Write("pos", "line=" + line); + } + + private static float ParseF(string s) + { + return float.Parse(s, CultureInfo.InvariantCulture); + } + + private void OnArduinoConnectionChanged(bool connected) + { + if (!connected) + { + _hasPosition = false; + MapArea.QueueDraw(); + } + } + + /// Maps the live Arduino (x,y) into BODY PIXELS using the same + /// bounding-box scale/offset DrawCircles uses, so the cursor tracks + /// the same coordinate frame as the numbered circles. + private bool TryGetCursorBodyPoint(out double bx, out double by) + { + bx = by = 0; + if (!_hasPosition || _jacket == null || _body == null) return false; + + var circles = _jacket.Get(JacketSize, _view); + if (circles.Count == 0) return false; + + var t = T; + int panelW = _body.Width; + int panelH = _body.Height; + + float minX = float.MaxValue, maxX = float.MinValue; + float minY = float.MaxValue, maxY = float.MinValue; + foreach (var c in circles) + { + if (c.X < minX) minX = c.X; + if (c.X > maxX) maxX = c.X; + if (c.Y < minY) minY = c.Y; + if (c.Y > maxY) maxY = c.Y; + } + + double availW = panelW - t.FitMargin, availH = panelH - t.FitMargin; + const double circleRadius = 2.1 / 2; + double dataW = (maxX - minX) + circleRadius * 2; + double dataH = (maxY - minY) + circleRadius * 2; + if (dataW <= 0 || dataH <= 0) return false; + + double scale = Math.Min(availW / dataW, availH / dataH) * 0.9; + + double offsetX = (panelW - (maxX - minX) * scale) / 2 - minX * scale; + double offsetY = (panelH - (maxY - minY) * scale) / 2 - minY * scale; + + // Form2: raw Arduino units run ~50x smaller than CSV/circle units. + bx = (_currentX * 50 + t.MarginX) * scale + offsetX; + by = (_currentY * 50 + t.MarginY) * scale + offsetY; + + if (JacketSize == "XL" && (_view == "front" || _view == "right" || _view == "left")) + { + double centerX = panelW / 2.0; + double offsetFromCenter = bx - centerX; + bx = centerX - offsetFromCenter; + } + + return true; + } + + private void DrawCursor(Cairo.Context cr, double x, double y) + { + const double size = 14; + + cr.NewPath(); + // cr.SetSourceRGBA(1.0, 0.0, 0.0, 0.9); //red not that visible + cr.SetSourceRGBA(1.0, 0.85, 0.0, 0.95); //yellow + cr.LineWidth = 2.5; + cr.MoveTo(x - size, y); + cr.LineTo(x + size, y); + cr.MoveTo(x, y - size); + cr.LineTo(x, y + size); + cr.Stroke(); + + cr.NewPath(); + cr.Arc(x, y, 5, 0, 2 * Math.PI); + cr.Stroke(); + } + + private void DrawCoordHud(Cairo.Context cr, int w, int h) + { + string text = string.Format(CultureInfo.InvariantCulture, + "X={0:F2} Y={1:F2} Z={2:F2} view={3}", _currentX, _currentY, _currentZ, _view); + + cr.SelectFontFace("Monospace", Cairo.FontSlant.Normal, Cairo.FontWeight.Bold); + cr.SetFontSize(13); + var ext = cr.TextExtents(text); + + double pad = 6, bx = 8, by = 8; + double bw = ext.Width + pad * 2, bh = ext.Height + pad * 2; + + cr.NewPath(); + cr.Rectangle(bx, by, bw, bh); + cr.SetSourceRGBA(0, 0, 0, 0.55); + cr.Fill(); + + cr.MoveTo(bx + pad - ext.XBearing, by + pad + ext.Height); + cr.SetSourceRGBA(1, 1, 1, 1); + cr.ShowText(text); + } + // ── public API ───────────────────────────────────────────────────── public void ShowCase(CaseDefinition def, string title) @@ -328,6 +496,10 @@ if (ShowCircles) DrawCircles(cr); // body-pixel coords; transform does the rest + double cx, cy; + if (TryGetCursorBodyPoint(out cx, out cy)) + DrawCursor(cr, cx, cy); + if (ShowGuides) { if (haveMap) @@ -350,6 +522,10 @@ } cr.Restore(); + // NEW — screen-space readout, unaffected by the body scale/translate above + if (_hasPosition) + DrawCoordHud(cr, w, h); //show the coordinate on top of the image + args.RetVal = true; } @@ -502,6 +678,10 @@ AddSpin(col, "Cluster", "Cluster factor", 0.05, 2.00, 0.05, 2); AddSpin(col, "FitMargin", "Fit margin (px)", 0, 400, 5, 0); + col.PackStart(Header("Cursor (Arduino)"), false, false, 0); + AddSpin(col, "MarginX", "Margin X (raw units)", -2000, 2000, 1, 0); + AddSpin(col, "MarginY", "Margin Y (raw units)", -2000, 2000, 1, 0); + col.PackStart(Header("Display"), false, false, 0); _tShowBody = Chk(col, "Show body", true); _tShowMap = Chk(col, "Show map", true); @@ -644,6 +824,8 @@ case "MapY": t.MapY = v; break; case "MapAlpha": t.MapAlpha = v; break; case "FitMargin": t.FitMargin = v; break; + case "MarginX": t.MarginX = v; break; + case "MarginY": t.MarginY = v; break; } } @@ -661,6 +843,8 @@ case "MapY": return t.MapY; case "MapAlpha": return t.MapAlpha; case "FitMargin": return t.FitMargin; + case "MarginX": return t.MarginX; + case "MarginY": return t.MarginY; } return 0; } @@ -1047,6 +1231,8 @@ public void Dispose() { + _arduino.LineReceived -= OnArduinoLine; + _arduino.ConnectionChanged -= OnArduinoConnectionChanged; _sound.Dispose(); DisposePixbufs(); if (_tuner != null) { _tuner.Destroy(); _tuner = null; } @@ -1056,4 +1242,32 @@ { get { return (JacketSizeCombo != null ? JacketSizeCombo.ActiveText : null) ?? "L"; } } + + private string DetermineViewFromZ(float z) + { + float frontDiff = Math.Abs(z - FRONT_Z); + float backDiff = Math.Abs(z - BACK_Z); + float rightDiff = Math.Abs(z - RIGHT_Z); + float leftDiff = Math.Abs(z - LEFT_Z); + + float minDiff = Math.Min(Math.Min(frontDiff, backDiff), Math.Min(rightDiff, leftDiff)); + if (minDiff > Z_TOLERANCE) return _view; // no clean match — stay put + + if (minDiff == frontDiff) return "front"; + if (minDiff == backDiff) return "back"; + if (minDiff == rightDiff) return "right"; + return "left"; + } + + /// Drives the same radio buttons a manual click would, so the UI and + /// _view stay in sync (SetView no-ops if it's already the active view). + private void SwitchViewFromArduino(string view) + { + RadioButton rb = view == "back" ? ViewBackRadio + : view == "right" ? ViewRightRadio + : view == "left" ? ViewLeftRadio + : ViewFrontRadio; + if (rb != null) rb.Active = true; + else SetView(view); + } } \ No newline at end of file