Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Sources/TouchpadInputApp/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -46,9 +46,19 @@ struct ContentView: View {
.frame(width: 340)
}
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)
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/TouchpadInputCore/Calibration/UserCalibration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 11 additions & 11 deletions Sources/TouchpadInputCore/DefaultImplementations/KeyGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 }
}
22 changes: 11 additions & 11 deletions Sources/TouchpadInputCore/Session/TouchInputSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,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()
)
Expand All @@ -131,9 +130,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()
}

Expand Down Expand Up @@ -274,8 +272,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)
Expand All @@ -293,7 +296,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
Expand Down Expand Up @@ -328,9 +331,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(
Expand Down
Loading