feat(typing): improve key hit accuracy and bump to 1.6.1

Add gap-filling hit regions, release-to-commit with slide-to-reselect,
touch intent offset, and full-pinyin next-key bias; cut release 1.6.1 (build 45).
This commit is contained in:
Rocky
2026-08-05 22:47:20 +08:00
parent 717902716a
commit 2ed16e1fbf
13 changed files with 1395 additions and 233 deletions
@@ -0,0 +1,184 @@
// KeyHitTestingTests.swift
// OSGKeyboard · Ext unit tests
//
// Phase 1 / 3: gap fill, nearest-center, intent offset, edge expansion.
import XCTest
@testable import OSGKeyboardShared
final class KeyHitTestingTests: XCTestCase {
private func makePairTargets(
left: CGRect = CGRect(x: 0, y: 0, width: 40, height: 50),
gap: CGFloat = 6
) -> (targets: [TypingKeyHitTarget], plane: CGRect) {
let right = CGRect(
x: left.maxX + gap,
y: left.minY,
width: left.width,
height: left.height
)
let targets = [
TypingKeyHitTarget(
id: "L",
label: "A",
visualFrame: left,
behavior: .commitOnRelease
),
TypingKeyHitTarget(
id: "R",
label: "S",
visualFrame: right,
behavior: .commitOnRelease
)
]
let plane = left.union(right)
return (targets, plane)
}
func testGapMidpointHitsNearestKey() {
let pair = makePairTargets()
let mid = CGPoint(x: 43, y: 25) // center of 6pt gap between 40 and 46
let hit = KeyHitTesting.hitTarget(
at: mid,
targets: pair.targets,
keyPlaneBounds: pair.plane,
horizontalGap: 6,
verticalGap: 7,
edgeExpansion: 0
)
// Midpoint is equidistant; either is acceptable, but must not miss.
XCTAssertNotNil(hit)
}
func testGapCloserToLeftSelectsLeft() {
let pair = makePairTargets()
// Just right of left key visual edge, still in gap, closer to left center.
let point = CGPoint(x: 41, y: 25)
let hit = KeyHitTesting.hitTarget(
at: point,
targets: pair.targets,
keyPlaneBounds: pair.plane,
horizontalGap: 6,
verticalGap: 7,
edgeExpansion: 0
)
XCTAssertEqual(hit?.id, "L")
}
func testGapCloserToRightSelectsRight() {
let pair = makePairTargets()
let point = CGPoint(x: 45, y: 25)
let hit = KeyHitTesting.hitTarget(
at: point,
targets: pair.targets,
keyPlaneBounds: pair.plane,
horizontalGap: 6,
verticalGap: 7,
edgeExpansion: 0
)
XCTAssertEqual(hit?.id, "R")
}
func testOutsidePlaneReturnsNil() {
let pair = makePairTargets()
let hit = KeyHitTesting.hitTarget(
at: CGPoint(x: 200, y: 200),
targets: pair.targets,
keyPlaneBounds: pair.plane,
horizontalGap: 6,
verticalGap: 7,
edgeExpansion: 0
)
XCTAssertNil(hit)
}
func testIntentOffsetShiftsHitUpward() {
// Key occupies y 1060. Raw touch at y=62 is below the key; with a
// 4pt upward intent offset it maps to y=58 and should still hit.
let target = TypingKeyHitTarget(
id: "K",
label: "M",
visualFrame: CGRect(x: 0, y: 10, width: 40, height: 50),
behavior: .commitOnRelease
)
let plane = target.visualFrame
let raw = CGPoint(x: 20, y: 62)
let withoutOffset = KeyHitTesting.hitTarget(
at: raw,
targets: [target],
keyPlaneBounds: plane,
horizontalGap: 0,
verticalGap: 0,
edgeExpansion: 0
)
let withOffset = KeyHitTesting.hitTarget(
rawTouch: raw,
targets: [target],
keyPlaneBounds: plane,
horizontalGap: 0,
verticalGap: 0,
intentOffsetY: 4,
edgeExpansion: 0
)
XCTAssertNil(withoutOffset)
XCTAssertEqual(withOffset?.id, "K")
}
func testEdgeExpansionExtendsOuterHit() {
let target = TypingKeyHitTarget(
id: "Q",
label: "Q",
visualFrame: CGRect(x: 10, y: 0, width: 40, height: 50),
behavior: .commitOnRelease
)
let plane = target.visualFrame
// Point just left of the visual key, inside edge expansion.
let point = CGPoint(x: 7, y: 25)
let hit = KeyHitTesting.hitTarget(
at: point,
targets: [target],
keyPlaneBounds: plane,
horizontalGap: 0,
verticalGap: 0,
edgeExpansion: 5
)
XCTAssertEqual(hit?.id, "Q")
}
func testLayoutBuilderCoversGapsBetweenKeys() {
let layout = TypingKeyLayoutBuilder.build(
size: CGSize(width: 300, height: 200),
letterRows: [
["Q", "W", "E"],
["A", "S", "D"],
["Z", "X", "C"]
],
pageSwitchLabel: "123",
spaceLabel: "space",
returnLabel: "return",
keyWeight: { _, _, _ in 1 }
)
XCTAssertEqual(layout.keys.count, 12) // 9 letters + 3 bottom
// Mid-gap between Q and W on first row should hit something.
let q = layout.keys.first { $0.label == "Q" }!
let w = layout.keys.first { $0.label == "W" }!
let mid = CGPoint(x: (q.visualFrame.maxX + w.visualFrame.minX) / 2, y: q.center.y)
let hit = KeyHitTesting.hitTarget(
at: mid,
targets: layout.keys,
keyPlaneBounds: layout.keyPlaneBounds,
horizontalGap: layout.horizontalGap,
verticalGap: layout.verticalGap
)
XCTAssertNotNil(hit)
XCTAssertTrue(hit?.label == "Q" || hit?.label == "W")
}
func testBehaviorResolver() {
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: ""), .deleteRepeat)
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: ""), .shiftHold)
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "A"), .commitOnRelease)
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "123"), .commitOnRelease)
}
}