mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Add pre-upload validation for pattern bounds vs hoop size
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
eadbecc401
commit
6534684967
2 changed files with 72 additions and 12 deletions
|
|
@ -267,6 +267,7 @@ function App() {
|
||||||
pesData={pesData}
|
pesData={pesData}
|
||||||
currentFileName={currentFileName}
|
currentFileName={currentFileName}
|
||||||
isUploading={machine.isUploading}
|
isUploading={machine.isUploading}
|
||||||
|
machineInfo={machine.machineInfo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter';
|
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 { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||||
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
@ -21,6 +21,7 @@ interface FileUploadProps {
|
||||||
pesData: PesPatternData | null;
|
pesData: PesPatternData | null;
|
||||||
currentFileName: string;
|
currentFileName: string;
|
||||||
isUploading?: boolean;
|
isUploading?: boolean;
|
||||||
|
machineInfo: MachineInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileUpload({
|
export function FileUpload({
|
||||||
|
|
@ -37,6 +38,7 @@ export function FileUpload({
|
||||||
pesData: pesDataProp,
|
pesData: pesDataProp,
|
||||||
currentFileName,
|
currentFileName,
|
||||||
isUploading = false,
|
isUploading = false,
|
||||||
|
machineInfo,
|
||||||
}: FileUploadProps) {
|
}: FileUploadProps) {
|
||||||
const [localPesData, setLocalPesData] = useState<PesPatternData | null>(null);
|
const [localPesData, setLocalPesData] = useState<PesPatternData | null>(null);
|
||||||
const [fileName, setFileName] = useState<string>('');
|
const [fileName, setFileName] = useState<string>('');
|
||||||
|
|
@ -95,6 +97,51 @@ export function FileUpload({
|
||||||
}
|
}
|
||||||
}, [pesData, displayFileName, onUpload, patternOffset]);
|
}, [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 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';
|
const iconColor = pesData ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400';
|
||||||
|
|
||||||
|
|
@ -189,13 +236,7 @@ export function FileUpload({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pesData && !canUploadPattern(machineStatus) && (
|
<div className="flex gap-2 mb-3">
|
||||||
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 rounded border border-yellow-200 dark:border-yellow-800 mb-3 text-xs">
|
|
||||||
Cannot upload while {getMachineStateCategory(machineStatus)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pes"
|
accept=".pes"
|
||||||
|
|
@ -207,7 +248,7 @@ export function FileUpload({
|
||||||
<label
|
<label
|
||||||
htmlFor={fileService.hasNativeDialogs() ? undefined : "file-input"}
|
htmlFor={fileService.hasNativeDialogs() ? undefined : "file-input"}
|
||||||
onClick={fileService.hasNativeDialogs() ? () => handleFileChange() : undefined}
|
onClick={fileService.hasNativeDialogs() ? () => 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
|
!pyodideReady || isLoading || patternUploaded || isUploading
|
||||||
? 'opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white'
|
? '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'
|
: '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 && (
|
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!isConnected || isUploading}
|
disabled={!isConnected || isUploading || !boundsCheck.fits}
|
||||||
className="px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : boundsCheck.error || 'Upload pattern to machine'}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -267,6 +308,24 @@ export function FileUpload({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error/warning messages with smooth transition - placed after buttons */}
|
||||||
|
<div className="transition-all duration-200 ease-in-out overflow-hidden" style={{
|
||||||
|
maxHeight: (pesData && (boundsCheck.error || !canUploadPattern(machineStatus))) ? '200px' : '0px',
|
||||||
|
marginTop: (pesData && (boundsCheck.error || !canUploadPattern(machineStatus))) ? '12px' : '0px'
|
||||||
|
}}>
|
||||||
|
{pesData && !canUploadPattern(machineStatus) && (
|
||||||
|
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 rounded border border-yellow-200 dark:border-yellow-800 text-xs">
|
||||||
|
Cannot upload while {getMachineStateCategory(machineStatus)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pesData && boundsCheck.error && (
|
||||||
|
<div className="bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-200 px-3 py-2 rounded border border-red-200 dark:border-red-800 text-xs">
|
||||||
|
<strong>Pattern too large:</strong> {boundsCheck.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isUploading && uploadProgress < 100 && (
|
{isUploading && uploadProgress < 100 && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue