diff --git a/src/handlers/tools.zod.ts b/src/handlers/tools.zod.ts index 67d48eb..ec8b8b7 100644 --- a/src/handlers/tools.zod.ts +++ b/src/handlers/tools.zod.ts @@ -93,6 +93,19 @@ export function setupTools(server: Server, provider: AutomationProvider): void { default: { width: 1280, fit: 'contain' }, description: 'Resize options for the screenshot', }, + grid: { + oneOf: [{ type: 'boolean' }, { type: 'number', minimum: 10, maximum: 500 }], + default: false, + description: + 'Draw coordinate grid overlay (true for 100px spacing, or specify pixel spacing)', + }, + gridTransparency: { + type: 'number', + minimum: 0, + maximum: 100, + default: 50, + description: 'Grid line transparency (0 = fully transparent, 100 = fully opaque)', + }, }, }, }, diff --git a/src/providers/keysender/screen.ts b/src/providers/keysender/screen.ts index 55b76f9..ebe7ced 100644 --- a/src/providers/keysender/screen.ts +++ b/src/providers/keysender/screen.ts @@ -143,7 +143,9 @@ export class KeysenderScreenAutomation implements ScreenAutomation { handle: number; }; - this.logger.warn(`Using fallback window "${fallbackWindow.title}" with default view values`); + this.logger.warn( + `Using fallback window "${fallbackWindow.title}" with default view values`, + ); return { window: fallbackWindow, @@ -427,7 +429,7 @@ export class KeysenderScreenAutomation implements ScreenAutomation { (Math.abs(updatedView.width - width) > 20 || Math.abs(updatedView.height - height) > 20) ) { this.logger.warn( - `Resize may not have been successful. Requested: ${width}x${height}, Got: ${updatedView.width}x${updatedView.height}` + `Resize may not have been successful. Requested: ${width}x${height}, Got: ${updatedView.width}x${updatedView.height}`, ); } else if ( operationType === 'reposition' && @@ -436,7 +438,7 @@ export class KeysenderScreenAutomation implements ScreenAutomation { (Math.abs(updatedView.x - x) > 20 || Math.abs(updatedView.y - y) > 20) ) { this.logger.warn( - `Repositioning may not have been successful. Requested: (${x}, ${y}), Got: (${updatedView.x}, ${updatedView.y})` + `Repositioning may not have been successful. Requested: (${x}, ${y}), Got: (${updatedView.x}, ${updatedView.y})`, ); } } catch (viewError) { @@ -599,6 +601,54 @@ export class KeysenderScreenAutomation implements ScreenAutomation { }); } + // Draw grid overlay if requested + if (mergedOptions.grid) { + const gridSpacing = typeof mergedOptions.grid === 'number' ? mergedOptions.grid : 100; + + // Calculate final image dimensions from resize settings + const targetWidth = mergedOptions.resize?.width || 1280; + const imgWidth = Math.min(width, targetWidth); + const imgHeight = Math.round(height * (imgWidth / width)); + + // Get window position offset to show true screen coordinates + let offsetX = 0; + let offsetY = 0; + try { + const viewInfo = this.hardware.workwindow.getView(); + offsetX = viewInfo.x || 0; + offsetY = viewInfo.y || 0; + } catch { + // If we can't get window position, use 0 offset (full screen capture) + } + + // Calculate scale factor (image pixels to screen pixels) + const scaleX = width / imgWidth; + const scaleY = height / imgHeight; + + // Calculate grid opacity from transparency (0-100 -> 0-1) + const gridOpacity = (mergedOptions.gridTransparency ?? 50) / 100; + const textOpacity = Math.min(1, gridOpacity + 0.3); // Text slightly more visible + + // Build SVG grid with screen coordinates (accounting for window offset and scale) + let svgLines = ''; + for (let x = gridSpacing; x < imgWidth; x += gridSpacing) { + const screenX = Math.round(offsetX + x * scaleX); + svgLines += ``; + svgLines += `${screenX}`; + } + for (let y = gridSpacing; y < imgHeight; y += gridSpacing) { + const screenY = Math.round(offsetY + y * scaleY); + svgLines += ``; + svgLines += `${screenY}`; + } + + const svgOverlay = Buffer.from( + `${svgLines}`, + ); + + pipeline = pipeline.composite([{ input: svgOverlay, top: 0, left: 0 }]); + } + // Apply appropriate format-specific compression if (mergedOptions.format === 'jpeg') { pipeline = pipeline.jpeg({ diff --git a/src/tools/screenshot.test.ts b/src/tools/screenshot.test.ts index 04a1295..61f345a 100644 --- a/src/tools/screenshot.test.ts +++ b/src/tools/screenshot.test.ts @@ -126,5 +126,59 @@ describe('Screenshot Functions', () => { message: 'Failed to capture screenshot: Capture failed', }); }); + + it('should pass grid options to provider', async () => { + // Setup mock provider + const mockProvider = { + screen: { + getScreenshot: vi.fn().mockResolvedValue({ + success: true, + message: 'Screenshot captured successfully', + content: [ + { + type: 'image', + data: 'test-image-with-grid', + mimeType: 'image/png', + }, + ], + }), + }, + }; + + vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any); + + const options = { + grid: true, + gridTransparency: 75, + }; + + const result = await getScreenshot(options); + + expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options); + expect(result.success).toBe(true); + }); + + it('should pass grid spacing as number to provider', async () => { + const mockProvider = { + screen: { + getScreenshot: vi.fn().mockResolvedValue({ + success: true, + message: 'Screenshot captured successfully', + content: [{ type: 'image', data: 'test', mimeType: 'image/png' }], + }), + }, + }; + + vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any); + + const options = { + grid: 200, // Custom grid spacing + gridTransparency: 30, + }; + + await getScreenshot(options); + + expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options); + }); }); }); diff --git a/src/tools/validation.zod.ts b/src/tools/validation.zod.ts index 7743d59..da44990 100644 --- a/src/tools/validation.zod.ts +++ b/src/tools/validation.zod.ts @@ -306,6 +306,8 @@ export const ScreenshotOptionsSchema = z.object({ grayscale: z.boolean().optional(), resize: ScreenshotResizeSchema.optional(), compressionLevel: z.number().int().min(0).max(9).optional(), + grid: z.union([z.boolean(), z.number().int().min(10).max(500)]).optional(), + gridTransparency: z.number().int().min(0).max(100).optional(), }); /** diff --git a/src/types/common.ts b/src/types/common.ts index d991519..708d78a 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -87,4 +87,6 @@ export interface ScreenshotOptions { fit?: 'contain' | 'cover' | 'fill' | 'inside' | 'outside'; // Resize fit option }; compressionLevel?: number; // PNG compression level (0-9), only used if format is 'png' + grid?: boolean | number; // Draw coordinate grid overlay (true for 100px, or specify spacing) + gridTransparency?: number; // Grid line transparency (0-100, default 50) }