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:
@@ -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 10…60. 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user