UNPKG

xc-mcp

Version:

MCP server that wraps Xcode command-line tools for iOS/macOS development workflows

193 lines 7.65 kB
/** * Coordinate Type System * * Why: IDB and simctl commands require integer coordinates, not floats. * Prevent floating point precision issues (e.g., 964.8000000000001) that cause CLI errors. * * Usage: * - Use IntCoordinate for all x/y values passed to IDB/simctl * - Use toInt() to safely convert floats to integers * - Use validateCoordinate() for input validation */ /** * Convert any number to integer coordinate * * Why: Screen dimension calculations often produce floats (e.g., screenW * 0.8). * Math.round() ensures clean integers for CLI commands. * * @param value - Number to convert (can be float) * @returns Integer coordinate safe for IDB/simctl */ export function toInt(value) { return Math.round(value); } /** * Validate coordinate is within screen bounds * * Why: Prevent out-of-bounds taps that always fail. * Provides clear error messages vs cryptic CLI failures. * * @param x - X coordinate * @param y - Y coordinate * @param screenWidth - Screen width in pixels * @param screenHeight - Screen height in pixels * @throws Error if coordinates out of bounds */ export function validateCoordinate(x, y, screenWidth, screenHeight) { if (x < 0 || x > screenWidth) { throw new Error(`X coordinate ${x} out of bounds (screen width: ${screenWidth}). Valid range: 0-${screenWidth}`); } if (y < 0 || y > screenHeight) { throw new Error(`Y coordinate ${y} out of bounds (screen height: ${screenHeight}). Valid range: 0-${screenHeight}`); } } /** * Swipe profiles for different gesture types * * Empirically tested on iOS 18.5 (iPhone 16 Pro Simulator, 393×852 points) * * Why: Different UIs respond to different gesture velocities. * - Standard: Balanced velocity (1475 pts/sec) - perfect for general navigation * - Flick: Fast page changes (2775 pts/sec) - for carousel and rapid navigation * - Gentle: Slow scrolling (653 pts/sec) - reliable at near-minimum threshold * * IMPORTANT: Minimum threshold is ~667 points/sec. Below this, iOS doesn't recognize swipes. * All profiles tested and verified working on iOS home screen (page navigation). */ export const SWIPE_PROFILES = { standard: { name: 'standard', description: 'Default swipe - best for general navigation (1475 pts/sec)', distancePercent: 0.75, // 295 points for 393w screen durationSeconds: 0.2, // 200ms calculatedVelocity: 1475, // 295 / 0.20 = 1475 points/sec }, flick: { name: 'flick', description: 'Fast swipe - snappy and responsive (2775 pts/sec)', distancePercent: 0.85, // 333 points for 393w screen durationSeconds: 0.12, // 120ms calculatedVelocity: 2775, // 333 / 0.12 = 2775 points/sec }, gentle: { name: 'gentle', description: 'Slow swipe - reliable but near-minimum threshold (653 pts/sec)', distancePercent: 0.5, // 196 points for 393w screen durationSeconds: 0.3, // 300ms calculatedVelocity: 653, // 196 / 0.30 = 653 points/sec }, }; /** * Calculate swipe velocity in points per second * * Why: iOS requires >650 points/sec for reliable recognition. * Low velocity swipes are interpreted as drags, not swipes. * Uses POINT space (393×852), not pixel space (1179×2556). * * @param distance - Distance traveled in points * @param durationMs - Duration in milliseconds * @returns Velocity in points per second */ export function calculateSwipeVelocity(distance, durationMs) { if (durationMs <= 0) { throw new Error('Duration must be positive'); } return (distance / durationMs) * 1000; // Convert ms to seconds } /** * Validate swipe velocity meets iOS minimum threshold * * Why: iOS requires minimum velocity for swipe recognition. * Returns warning if velocity is too low, but execution proceeds. * Empirically tested minimum: ~667 points/sec * * @param velocity - Velocity in points per second * @param _profile - Swipe profile for context * @returns Validation result with warning if velocity too low */ export function validateSwipeVelocity(velocity, _profile) { const minVelocity = 750; // Conservative minimum for reliable swipe recognition (empirically ~667) if (velocity < minVelocity) { return { valid: false, warning: `Swipe velocity ${velocity.toFixed(0)} points/sec is below recommended minimum of ${minVelocity} points/sec. Gesture may not be recognized reliably.`, }; } return { valid: true }; } /** * Calculate directional swipe coordinates with optional profile * * CRITICAL: Uses POINT coordinates (393×852 for iPhone 16 Pro), NOT pixel coordinates. * IDB's swipe command expects coordinates in point space. * * Why: Convert high-level directions to precise integer coordinates using empirically tested * absolute coordinates for each profile. This ensures reliable swipe recognition across all * profiles and ensures consistency with Grapla reference implementation. * * Profiles use absolute coordinates (iPhone 16 Pro 393×852): * - Standard: Balanced (350→50 horizontal, 750→150 vertical, 1475 pts/sec) * - Flick: Fast (360→30 horizontal, 780→120 vertical, 2775 pts/sec) * - Gentle: Slow (300→100 horizontal, 650→250 vertical, 653 pts/sec) * * @param direction - Swipe direction ('up', 'down', 'left', 'right') * @param screenWidth - Screen width in POINTS (393 for iPhone 16 Pro), NOT pixels * @param screenHeight - Screen height in POINTS (852 for iPhone 16 Pro), NOT pixels * @param profile - Optional swipe profile (defaults to 'standard') * @returns Integer start and end coordinates in POINT space (absolute, not relative to screen size) */ export function calculateSwipeCoordinates(direction, screenWidth, screenHeight, profile) { // Use absolute coordinates based on profile (empirically tested values for iPhone 16 Pro) // These coordinates are fixed and work best with the 393×852 point dimensions const profileCoordinates = { standard: { horizontalStart: 350, horizontalEnd: 50, verticalStart: 750, verticalEnd: 150, }, flick: { horizontalStart: 360, horizontalEnd: 30, verticalStart: 780, verticalEnd: 120, }, gentle: { horizontalStart: 300, horizontalEnd: 100, verticalStart: 650, verticalEnd: 250, }, }; const selectedProfile = profile || 'standard'; const coords = profileCoordinates[selectedProfile]; const centerX = toInt(screenWidth / 2); const centerY = toInt(screenHeight / 2); switch (direction) { case 'up': // Swipe from bottom to top return { start: { x: centerX, y: coords.verticalStart }, end: { x: centerX, y: coords.verticalEnd }, }; case 'down': // Swipe from top to bottom return { start: { x: centerX, y: coords.verticalEnd }, end: { x: centerX, y: coords.verticalStart }, }; case 'left': // Swipe from right to left return { start: { x: coords.horizontalStart, y: centerY }, end: { x: coords.horizontalEnd, y: centerY }, }; case 'right': // Swipe from left to right return { start: { x: coords.horizontalEnd, y: centerY }, end: { x: coords.horizontalStart, y: centerY }, }; } } //# sourceMappingURL=coordinates.js.map