From 65346849677655be4ba7d2389d9de09aa68a70e8 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Wed, 10 Dec 2025 14:18:45 +0100 Subject: [PATCH] Add pre-upload validation for pattern bounds vs hoop size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate pattern (with offset) fits within hoop bounds before upload - Calculate precise overflow in each direction (left, right, top, bottom) - Display detailed error message showing exact measurements - Disable upload button when pattern exceeds hoop bounds - Position error messages below buttons with smooth slide animation - Set button sizing: file select (2/3), upload (1/3) for consistent layout - Pass machineInfo to FileUpload component for hoop dimensions Prevents uploading patterns that would exceed machine working area and provides clear feedback on how to adjust pattern position. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/App.tsx | 1 + src/components/FileUpload.tsx | 83 ++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b06f29d..6b8c206 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -267,6 +267,7 @@ function App() { pesData={pesData} currentFileName={currentFileName} isUploading={machine.isUploading} + machineInfo={machine.machineInfo} /> )} diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index e7b9635..e2f7500 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter'; -import { MachineStatus } from '../types/machine'; +import { MachineStatus, type MachineInfo } from '../types/machine'; import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers'; import { PatternInfoSkeleton } from './SkeletonLoader'; import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid'; @@ -21,6 +21,7 @@ interface FileUploadProps { pesData: PesPatternData | null; currentFileName: string; isUploading?: boolean; + machineInfo: MachineInfo | null; } export function FileUpload({ @@ -37,6 +38,7 @@ export function FileUpload({ pesData: pesDataProp, currentFileName, isUploading = false, + machineInfo, }: FileUploadProps) { const [localPesData, setLocalPesData] = useState(null); const [fileName, setFileName] = useState(''); @@ -95,6 +97,51 @@ export function FileUpload({ } }, [pesData, displayFileName, onUpload, patternOffset]); + // Check if pattern (with offset) fits within hoop bounds + const checkPatternFitsInHoop = useCallback(() => { + if (!pesData || !machineInfo) { + return { fits: true, error: null }; + } + + const { bounds } = pesData; + const { maxWidth, maxHeight } = machineInfo; + + // Calculate pattern bounds with offset applied + const patternMinX = bounds.minX + patternOffset.x; + const patternMaxX = bounds.maxX + patternOffset.x; + const patternMinY = bounds.minY + patternOffset.y; + const patternMaxY = bounds.maxY + patternOffset.y; + + // Hoop bounds (centered at origin) + const hoopMinX = -maxWidth / 2; + const hoopMaxX = maxWidth / 2; + const hoopMinY = -maxHeight / 2; + const hoopMaxY = maxHeight / 2; + + // Check if pattern exceeds hoop bounds + const exceedsLeft = patternMinX < hoopMinX; + const exceedsRight = patternMaxX > hoopMaxX; + const exceedsTop = patternMinY < hoopMinY; + const exceedsBottom = patternMaxY > hoopMaxY; + + if (exceedsLeft || exceedsRight || exceedsTop || exceedsBottom) { + const directions = []; + if (exceedsLeft) directions.push(`left by ${((hoopMinX - patternMinX) / 10).toFixed(1)}mm`); + if (exceedsRight) directions.push(`right by ${((patternMaxX - hoopMaxX) / 10).toFixed(1)}mm`); + if (exceedsTop) directions.push(`top by ${((hoopMinY - patternMinY) / 10).toFixed(1)}mm`); + if (exceedsBottom) directions.push(`bottom by ${((patternMaxY - hoopMaxY) / 10).toFixed(1)}mm`); + + return { + fits: false, + error: `Pattern exceeds hoop bounds: ${directions.join(', ')}. Adjust pattern position in preview.` + }; + } + + return { fits: true, error: null }; + }, [pesData, machineInfo, patternOffset]); + + const boundsCheck = checkPatternFitsInHoop(); + const borderColor = pesData ? 'border-orange-600 dark:border-orange-500' : 'border-gray-400 dark:border-gray-600'; const iconColor = pesData ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400'; @@ -189,13 +236,7 @@ export function FileUpload({ )} - {pesData && !canUploadPattern(machineStatus) && ( -
- Cannot upload while {getMachineStateCategory(machineStatus)} -
- )} - -
+
handleFileChange() : undefined} - className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded font-semibold text-xs transition-all ${ + className={`flex-[2] flex items-center justify-center gap-2 px-3 py-2 rounded font-semibold text-xs transition-all ${ !pyodideReady || isLoading || patternUploaded || isUploading ? 'opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white' : 'cursor-pointer bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600' @@ -245,9 +286,9 @@ export function FileUpload({ {pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
+ {/* Error/warning messages with smooth transition - placed after buttons */} +
+ {pesData && !canUploadPattern(machineStatus) && ( +
+ Cannot upload while {getMachineStateCategory(machineStatus)} +
+ )} + + {pesData && boundsCheck.error && ( +
+ Pattern too large: {boundsCheck.error} +
+ )} +
+ {isUploading && uploadProgress < 100 && (