From 84638c7cf6eb04cda61040c63c3ab562f0297d90 Mon Sep 17 00:00:00 2001 From: Julio Maniratunga <694912+julio73@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:18:13 +0000 Subject: [PATCH 1/2] fix: apply calibration as global tap-coordinate offset instead of shifting zone boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach shifted each key zone's boundaries by its per-key offset, which broke grid tiling — adjacent zones would overlap or leave gaps, causing wrong-key lookups. New approach: compute a single global (median) dx/dy from the per-key calibration samples, then subtract it from the raw tap coordinate before zone lookup. The grid boundaries stay intact and perfectly tiled at all times. --- .../TouchpadInputApp/Views/ContentView.swift | 2 +- .../Calibration/UserCalibration.swift | 10 +++++++++ .../DefaultImplementations/KeyGrid.swift | 22 +++++++++---------- .../Session/TouchInputSession.swift | 22 +++++++++---------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/Sources/TouchpadInputApp/Views/ContentView.swift b/Sources/TouchpadInputApp/Views/ContentView.swift index 5d4b724..e69916a 100644 --- a/Sources/TouchpadInputApp/Views/ContentView.swift +++ b/Sources/TouchpadInputApp/Views/ContentView.swift @@ -29,7 +29,7 @@ struct ContentView: View { TrackpadSurface( fingers: session.liveFingers, isActive: session.isActive, - zones: KeyGrid.default.applying(calibration: session.userCalibration).zones, + zones: KeyGrid.default.zones, activeModifiers: session.activeModifiers ) .padding(16) diff --git a/Sources/TouchpadInputCore/Calibration/UserCalibration.swift b/Sources/TouchpadInputCore/Calibration/UserCalibration.swift index 3ceee67..e399a36 100644 --- a/Sources/TouchpadInputCore/Calibration/UserCalibration.swift +++ b/Sources/TouchpadInputCore/Calibration/UserCalibration.swift @@ -18,6 +18,16 @@ public struct UserCalibration: Codable, Sendable { public static let empty = UserCalibration(offsets: [:]) + /// Median per-axis offset across all calibrated keys. Applied to the raw tap coordinate + /// before zone lookup so the grid boundaries stay intact (no overlap / gaps). + public var globalOffset: (dx: Float, dy: Float) { + guard !offsets.isEmpty else { return (0, 0) } + let dxs = offsets.values.map { $0.dx }.sorted() + let dys = offsets.values.map { $0.dy }.sorted() + let mid = offsets.count / 2 + return (dxs[mid], dys[mid]) + } + public init(offsets: [String: Offset]) { self.offsets = offsets } private static let defaultsKey = "userCalibration" diff --git a/Sources/TouchpadInputCore/DefaultImplementations/KeyGrid.swift b/Sources/TouchpadInputCore/DefaultImplementations/KeyGrid.swift index 8301d67..f211ed5 100644 --- a/Sources/TouchpadInputCore/DefaultImplementations/KeyGrid.swift +++ b/Sources/TouchpadInputCore/DefaultImplementations/KeyGrid.swift @@ -28,17 +28,12 @@ public struct KeyGrid: Sendable { zones.first { z in x >= z.xMin && x < z.xMax && y >= z.yMin && y < z.yMax } } - /// Returns a new grid with each zone's boundaries shifted by its calibration offset. - public func applying(calibration: UserCalibration) -> KeyGrid { - guard !calibration.offsets.isEmpty else { return self } - let shifted = zones.map { zone -> KeyZone in - let key = String(zone.character) - guard let off = calibration.offsets[key] else { return zone } - return KeyZone(character: zone.character, altCharacter: zone.altCharacter, - xMin: zone.xMin + off.dx, xMax: zone.xMax + off.dx, - yMin: zone.yMin + off.dy, yMax: zone.yMax + off.dy) - } - return KeyGrid(zones: shifted) + /// Looks up a zone after correcting for the user's global hand-position offset. + /// Adjusting the tap coordinate (rather than shifting zone boundaries) keeps the grid + /// perfectly tiled with no overlaps or gaps. + public func zone(at x: Float, y: Float, calibration: UserCalibration) -> KeyZone? { + let off = calibration.globalOffset + return zone(at: x - off.dx, y: y - off.dy) } public static let `default`: KeyGrid = { @@ -90,5 +85,10 @@ extension KeyGrid: InputZoneProvider { return String(z.character) } + public func zoneID(at x: Float, y: Float, calibration: UserCalibration) -> String? { + guard let z = zone(at: x, y: y, calibration: calibration) else { return nil } + return String(z.character) + } + public func label(forZoneID id: String) -> String? { id } } diff --git a/Sources/TouchpadInputCore/Session/TouchInputSession.swift b/Sources/TouchpadInputCore/Session/TouchInputSession.swift index 73b3f60..66a86d3 100644 --- a/Sources/TouchpadInputCore/Session/TouchInputSession.swift +++ b/Sources/TouchpadInputCore/Session/TouchInputSession.swift @@ -116,10 +116,9 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve public convenience init() { let cal = UserCalibration.load() - let calibratedGrid = KeyGrid.default.applying(calibration: cal) self.init( - zoneProvider: calibratedGrid, - resolver: CharacterEmitter(grid: calibratedGrid), + zoneProvider: KeyGrid.default, + resolver: CharacterEmitter(grid: .default), modifierStrategy: CornerModifierStrategy.default, completionProvider: SpellCheckerCompletionProvider() ) @@ -130,9 +129,8 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve public func applyCalibration(_ calibration: UserCalibration) { userCalibration = calibration - let calibratedGrid = KeyGrid.default.applying(calibration: calibration) - zoneProvider = calibratedGrid - resolver = CharacterEmitter(grid: calibratedGrid) + zoneProvider = KeyGrid.default + resolver = CharacterEmitter(grid: .default) calibration.save() } @@ -273,8 +271,13 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve if let calSession = activeCalibrationSession { calSession.recordTap(x: fx, y: fy) } else { + // Adjust tap by global calibration offset before zone lookup. + let calOff = userCalibration.globalOffset + let calX = fx - calOff.dx + let calY = fy - calOff.dy + // Suppress "began" only for modifier zones that are NOT already held. - let modifier = modifierStrategy.modifierKind(at: fx, y: fy) + let modifier = modifierStrategy.modifierKind(at: calX, y: calY) let isInUnheldModifierZone: Bool if let mod = modifier { isInUnheldModifierZone = !heldModifiers.contains(mod) @@ -292,7 +295,7 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve completions = completionProvider?.completions( forPartial: currentPartialWord, maxCount: 3 ) ?? [] - } else if let zoneID = zoneProvider.zoneID(at: fx, y: fy) { + } else if let zoneID = zoneProvider.zoneID(at: calX, y: calY) { if !emittedZoneKeys.contains(zoneID) { emittedZoneKeys.insert(zoneID) let passesSize = contact.size >= minContactSize @@ -326,9 +329,6 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve in: KeyGrid.default ) userCalibration.save() - let calibratedGrid = KeyGrid.default.applying(calibration: userCalibration) - zoneProvider = calibratedGrid - resolver = CharacterEmitter(grid: calibratedGrid) } completions = completionProvider?.completions( From 8f0236276b9d8bcde8ed39007840648c8402dd07 Mon Sep 17 00:00:00 2001 From: Julio Maniratunga <694912+julio73@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:07:45 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20resolve=20merge=20conflict=20in=20Co?= =?UTF-8?q?ntentView=20=E2=80=94=20remove=20deleted=20applying()=20call=20?= =?UTF-8?q?and=20fix=20else=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge of feat/configurable-force-press into fix/calibration-global-offset left two broken artifacts in ContentView: 1. KeyGrid.default.applying(calibration:).zones — applying() was removed by the calibration PR; replaced with KeyGrid.default.zones (offset now applied at tap-coordinate level in TouchInputSession) 2. } else { DrawingCanvasView } was attached to an HStack instead of the enclosing if appMode == .keyboard block; moved the Divider + output HStack inside the keyboard branch so the else correctly toggles drawing mode --- .../TouchpadInputApp/Views/ContentView.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/TouchpadInputApp/Views/ContentView.swift b/Sources/TouchpadInputApp/Views/ContentView.swift index 9d0d902..7b158d9 100644 --- a/Sources/TouchpadInputApp/Views/ContentView.swift +++ b/Sources/TouchpadInputApp/Views/ContentView.swift @@ -37,7 +37,7 @@ struct ContentView: View { TrackpadSurface( fingers: session.liveFingers, isActive: session.isActive, - zones: KeyGrid.default.applying(calibration: session.userCalibration).zones, + zones: KeyGrid.default.zones, activeModifiers: session.activeModifiers ) .padding(16) @@ -45,20 +45,20 @@ struct ContentView: View { FingerTablePanel(fingers: session.liveFingers) .frame(width: 340) } - } - Divider() - HStack(spacing: 0) { - TrackpadSurface( - fingers: session.liveFingers, - isActive: session.isActive, - zones: KeyGrid.default.zones, - activeModifiers: session.activeModifiers - ) - .padding(16) - Divider() - OutputBufferPanel(text: session.outputBuffer, activeModifiers: session.activeModifiers) Divider() - EventLogPanel(entries: session.eventLog) + HStack(spacing: 0) { + TrackpadSurface( + fingers: session.liveFingers, + isActive: session.isActive, + zones: KeyGrid.default.zones, + activeModifiers: session.activeModifiers + ) + .padding(16) + Divider() + OutputBufferPanel(text: session.outputBuffer, activeModifiers: session.activeModifiers) + Divider() + EventLogPanel(entries: session.eventLog) + } } else { DrawingCanvasView(session: drawSession) }