From 885e9cb119ebb814b65e25b2719a3c66232b4fd4 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Mon, 22 Sep 2025 12:52:58 +0100 Subject: [PATCH] Cleanups --- .../tools/crop/CropAreaSelector.tsx | 24 ++++--- .../components/tools/crop/CropSettings.tsx | 19 +++--- frontend/src/constants/cropConstants.ts | 54 +--------------- frontend/src/constants/pageSizeConstants.ts | 8 +++ .../src/hooks/tools/crop/useCropOperation.ts | 9 +-- .../src/hooks/tools/crop/useCropParameters.ts | 62 +++++++------------ frontend/src/utils/cropCoordinates.ts | 55 +++++++++------- 7 files changed, 88 insertions(+), 143 deletions(-) create mode 100644 frontend/src/constants/pageSizeConstants.ts diff --git a/frontend/src/components/tools/crop/CropAreaSelector.tsx b/frontend/src/components/tools/crop/CropAreaSelector.tsx index f48c48d33..75d326210 100644 --- a/frontend/src/components/tools/crop/CropAreaSelector.tsx +++ b/frontend/src/components/tools/crop/CropAreaSelector.tsx @@ -2,29 +2,27 @@ import React, { useRef, useState, useCallback, useEffect } from 'react'; import { Box, useMantineTheme, MantineTheme } from '@mantine/core'; import { PDFBounds, - CropArea, - DOMRect, + Rectangle, domToPDFCoordinates, pdfToDOMCoordinates, constrainDOMRectToThumbnail, isPointInThumbnail } from '../../../utils/cropCoordinates'; +import { type ResizeHandle } from '../../../constants/cropConstants'; interface CropAreaSelectorProps { /** PDF bounds for coordinate conversion */ pdfBounds: PDFBounds; /** Current crop area in PDF coordinates */ - cropArea: CropArea; + cropArea: Rectangle; /** Callback when crop area changes */ - onCropAreaChange: (cropArea: CropArea) => void; + onCropAreaChange: (cropArea: Rectangle) => void; /** Whether the selector is disabled */ disabled?: boolean; /** Child content (typically the PDF thumbnail) */ children: React.ReactNode; } -type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null; - const CropAreaSelector: React.FC = ({ pdfBounds, cropArea, @@ -39,7 +37,7 @@ const CropAreaSelector: React.FC = ({ const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(null); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); - const [initialCropArea, setInitialCropArea] = useState(cropArea); + const [initialCropArea, setInitialCropArea] = useState(cropArea); // Convert PDF crop area to DOM coordinates for display const domRect = pdfToDOMCoordinates(cropArea, pdfBounds); @@ -85,7 +83,7 @@ const CropAreaSelector: React.FC = ({ e.stopPropagation(); // Start new crop selection - const newDomRect: DOMRect = { x, y, width: 20, height: 20 }; + const newDomRect: Rectangle = { x, y, width: 20, height: 20 }; const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds); const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds); @@ -107,7 +105,7 @@ const CropAreaSelector: React.FC = ({ const newX = x - dragStart.x; const newY = y - dragStart.y; - const newDomRect: DOMRect = { + const newDomRect: Rectangle = { x: newX, y: newY, width: domRect.width, @@ -188,7 +186,7 @@ const CropAreaSelector: React.FC = ({ // Helper functions -function getResizeHandle(x: number, y: number, domRect: DOMRect): ResizeHandle { +function getResizeHandle(x: number, y: number, domRect: Rectangle): ResizeHandle { const handleSize = 8; const tolerance = handleSize; @@ -211,17 +209,17 @@ function isNear(a: number, b: number, tolerance: number): boolean { return Math.abs(a - b) <= tolerance; } -function isPointInCropArea(x: number, y: number, domRect: DOMRect): boolean { +function isPointInCropArea(x: number, y: number, domRect: Rectangle): boolean { return x >= domRect.x && x <= domRect.x + domRect.width && y >= domRect.y && y <= domRect.y + domRect.height; } function calculateResizedRect( handle: ResizeHandle, - currentRect: DOMRect, + currentRect: Rectangle, mouseX: number, mouseY: number, -): DOMRect { +): Rectangle { let { x, y, width, height } = currentRect; switch (handle) { diff --git a/frontend/src/components/tools/crop/CropSettings.tsx b/frontend/src/components/tools/crop/CropSettings.tsx index dcfb3fcec..493fe1890 100644 --- a/frontend/src/components/tools/crop/CropSettings.tsx +++ b/frontend/src/components/tools/crop/CropSettings.tsx @@ -5,10 +5,12 @@ import RestartAltIcon from "@mui/icons-material/RestartAlt"; import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters"; import { useSelectedFiles } from "../../../contexts/file/fileHooks"; import CropAreaSelector from "./CropAreaSelector"; +import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants"; +import { PAGE_SIZES } from "src/constants/pageSizeConstants"; import { calculatePDFBounds, PDFBounds, - CropArea + Rectangle } from "../../../utils/cropCoordinates"; import { pdfWorkerManager } from "../../../services/pdfWorkerManager"; import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail"; @@ -69,12 +71,7 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => { setPdfBounds(bounds); // Initialize crop area to full PDF if parameters are still default - const isDefault = parameters.parameters.width === 595 && - parameters.parameters.height === 842 && - parameters.parameters.x === 0 && - parameters.parameters.y === 0; - - if (isDefault) { + if (parameters.parameters.cropArea === DEFAULT_CROP_AREA) { parameters.resetToFullPDF(bounds); } @@ -83,10 +80,10 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => { } catch (error) { console.error('Failed to load PDF dimensions:', error); // Fallback to A4 dimensions if PDF loading fails - const bounds = calculatePDFBounds(595, 842, CONTAINER_SIZE, CONTAINER_SIZE); + const bounds = calculatePDFBounds(PAGE_SIZES.A4.width, PAGE_SIZES.A4.height, CONTAINER_SIZE, CONTAINER_SIZE); setPdfBounds(bounds); - if (parameters.parameters.width === 595 && parameters.parameters.height === 842) { + if (parameters.parameters.cropArea.width === PAGE_SIZES.A4.width && parameters.parameters.cropArea.height === PAGE_SIZES.A4.height) { parameters.resetToFullPDF(bounds); } } @@ -100,14 +97,14 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => { // Handle crop area changes from the selector - const handleCropAreaChange = (newCropArea: CropArea) => { + const handleCropAreaChange = (newCropArea: Rectangle) => { if (pdfBounds) { parameters.setCropArea(newCropArea, pdfBounds); } }; // Handle manual coordinate input changes - const handleCoordinateChange = (field: keyof CropArea, value: number | string) => { + const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) return; diff --git a/frontend/src/constants/cropConstants.ts b/frontend/src/constants/cropConstants.ts index 2787bb156..50352ae07 100644 --- a/frontend/src/constants/cropConstants.ts +++ b/frontend/src/constants/cropConstants.ts @@ -1,37 +1,4 @@ -/** - * Constants and configuration for the crop tool - */ - -// Default PDF page sizes in points (1 point = 1/72 inch) -export const PAGE_SIZES = { - A4: { width: 595, height: 842 }, - LETTER: { width: 612, height: 792 }, - A3: { width: 842, height: 1191 }, - A5: { width: 420, height: 595 }, - LEGAL: { width: 612, height: 1008 }, -} as const; - -// Minimum crop area dimensions (in points) -export const MIN_CROP_SIZE = { - width: 10, - height: 10, -} as const; - -// Maximum container size for thumbnail display -export const CROP_CONTAINER_SIZE = 400; - -// Crop overlay styling -export const CROP_OVERLAY = { - borderColor: '#ff4757', - backgroundColor: 'rgba(255, 71, 87, 0.1)', - borderWidth: 2, - handleSize: 8, - handleColor: '#ff4757', - handleBorderColor: 'white', -} as const; - -// Coordinate precision (decimal places) -export const COORDINATE_PRECISION = 1; +import { PAGE_SIZES } from "./pageSizeConstants"; // Default crop area (covers entire page) export const DEFAULT_CROP_AREA = { @@ -41,22 +8,5 @@ export const DEFAULT_CROP_AREA = { height: PAGE_SIZES.A4.height, } as const; -// Resize handle positions -export const RESIZE_HANDLES = [ - 'nw', 'ne', 'sw', 'se', // corners - 'n', 'e', 's', 'w' // edges -] as const; -export type ResizeHandle = typeof RESIZE_HANDLES[number]; - -// Cursor styles for resize handles -export const RESIZE_CURSORS: Record = { - 'nw': 'nw-resize', - 'ne': 'ne-resize', - 'sw': 'sw-resize', - 'se': 'se-resize', - 'n': 'n-resize', - 'e': 'e-resize', - 's': 's-resize', - 'w': 'w-resize', -} as const; +export type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null; diff --git a/frontend/src/constants/pageSizeConstants.ts b/frontend/src/constants/pageSizeConstants.ts new file mode 100644 index 000000000..66876baba --- /dev/null +++ b/frontend/src/constants/pageSizeConstants.ts @@ -0,0 +1,8 @@ +// Default PDF page sizes in points (1 point = 1/72 inch) +export const PAGE_SIZES = { + A4: { width: 595, height: 842 }, + LETTER: { width: 612, height: 792 }, + A3: { width: 842, height: 1191 }, + A5: { width: 420, height: 595 }, + LEGAL: { width: 612, height: 1008 }, +} as const; diff --git a/frontend/src/hooks/tools/crop/useCropOperation.ts b/frontend/src/hooks/tools/crop/useCropOperation.ts index 3a9b5e370..452b3ddf1 100644 --- a/frontend/src/hooks/tools/crop/useCropOperation.ts +++ b/frontend/src/hooks/tools/crop/useCropOperation.ts @@ -7,12 +7,13 @@ import { CropParameters, defaultParameters } from './useCropParameters'; export const buildCropFormData = (parameters: CropParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); + const cropArea = parameters.cropArea; // Backend expects precise float values for PDF coordinates - formData.append("x", parameters.x.toString()); - formData.append("y", parameters.y.toString()); - formData.append("width", parameters.width.toString()); - formData.append("height", parameters.height.toString()); + formData.append("x", cropArea.x.toString()); + formData.append("y", cropArea.y.toString()); + formData.append("width", cropArea.width.toString()); + formData.append("height", cropArea.height.toString()); return formData; }; diff --git a/frontend/src/hooks/tools/crop/useCropParameters.ts b/frontend/src/hooks/tools/crop/useCropParameters.ts index 2c2f7145a..fdfa6f453 100644 --- a/frontend/src/hooks/tools/crop/useCropParameters.ts +++ b/frontend/src/hooks/tools/crop/useCropParameters.ts @@ -1,31 +1,22 @@ import { BaseParameters } from '../../../types/parameters'; import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; import { useCallback } from 'react'; -import { CropArea, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea } from '../../../utils/cropCoordinates'; +import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates'; +import { DEFAULT_CROP_AREA } from 'src/constants/cropConstants'; export interface CropParameters extends BaseParameters { - /** X coordinate of crop area (PDF points, left edge) */ - x: number; - /** Y coordinate of crop area (PDF points, bottom edge in PDF coordinate system) */ - y: number; - /** Width of crop area (PDF points) */ - width: number; - /** Height of crop area (PDF points) */ - height: number; + cropArea: Rectangle; } export const defaultParameters: CropParameters = { - x: 0, - y: 0, - width: 595, // Default A4 width in points - height: 842, // Default A4 height in points + cropArea: DEFAULT_CROP_AREA, }; export type CropParametersHook = BaseParametersHook & { /** Set crop area with PDF bounds validation */ - setCropArea: (cropArea: CropArea, pdfBounds?: PDFBounds) => void; + setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void; /** Get current crop area as CropArea object */ - getCropArea: () => CropArea; + getCropArea: () => Rectangle; /** Reset to full PDF dimensions */ resetToFullPDF: (pdfBounds: PDFBounds) => void; /** Check if current crop area is valid for the PDF */ @@ -33,7 +24,7 @@ export type CropParametersHook = BaseParametersHook & { /** Check if crop area covers the entire PDF */ isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean; /** Update crop area with constraints applied */ - updateCropAreaConstrained: (cropArea: Partial, pdfBounds?: PDFBounds) => void; + updateCropAreaConstrained: (cropArea: Partial, pdfBounds?: PDFBounds) => void; }; export const useCropParameters = (): CropParametersHook => { @@ -41,37 +32,29 @@ export const useCropParameters = (): CropParametersHook => { defaultParameters, endpointName: 'crop', validateFn: (params) => { + const rect = params.cropArea; // Basic validation - coordinates and dimensions must be positive - return params.x >= 0 && - params.y >= 0 && - params.width > 0 && - params.height > 0; + return rect.x >= 0 && + rect.y >= 0 && + rect.width > 0 && + rect.height > 0; }, }); // Get current crop area as CropArea object - const getCropArea = useCallback((): CropArea => { - return { - x: baseHook.parameters.x, - y: baseHook.parameters.y, - width: baseHook.parameters.width, - height: baseHook.parameters.height, - }; + const getCropArea = useCallback((): Rectangle => { + return baseHook.parameters.cropArea; }, [baseHook.parameters]); // Set crop area with optional PDF bounds validation - const setCropArea = useCallback((cropArea: CropArea, pdfBounds?: PDFBounds) => { + const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => { let finalCropArea = roundCropArea(cropArea); // Apply PDF bounds constraints if provided if (pdfBounds) { finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds); } - - baseHook.updateParameter('x', finalCropArea.x); - baseHook.updateParameter('y', finalCropArea.y); - baseHook.updateParameter('width', finalCropArea.width); - baseHook.updateParameter('height', finalCropArea.height); + baseHook.updateParameter('cropArea', finalCropArea); }, [baseHook]); // Reset to cover entire PDF @@ -114,7 +97,7 @@ export const useCropParameters = (): CropParametersHook => { // Update crop area with constraints applied const updateCropAreaConstrained = useCallback(( - partialCropArea: Partial, + partialCropArea: Partial, pdfBounds?: PDFBounds ) => { const currentCropArea = getCropArea(); @@ -132,11 +115,12 @@ export const useCropParameters = (): CropParametersHook => { parameter: K, value: CropParameters[K] ) => { - // Ensure numeric parameters are positive - if (typeof value === 'number' && parameter !== 'x' && parameter !== 'y') { - value = Math.max(0.1, value) as CropParameters[K]; // Minimum 0.1 point - } else if (typeof value === 'number') { - value = Math.max(0, value) as CropParameters[K]; // x,y can be 0 + + if(isRectangle(value)) { + value.x = Math.max(0.1, value.x); // Minimum 0.1 point + value.x = Math.max(0.1, value.y); // Minimum 0.1 point + value.width = Math.max(0, value.width); // Minimum 0 point + value.height = Math.max(0, value.height); // Minimum 0 point } baseHook.updateParameter(parameter, value); diff --git a/frontend/src/utils/cropCoordinates.ts b/frontend/src/utils/cropCoordinates.ts index 6ad225572..a8c7192f4 100644 --- a/frontend/src/utils/cropCoordinates.ts +++ b/frontend/src/utils/cropCoordinates.ts @@ -19,26 +19,33 @@ export interface PDFBounds { scale: number; } -export interface CropArea { - /** X coordinate in PDF points (0 = left edge) */ +export interface Rectangle { + /** X coordinate */ x: number; - /** Y coordinate in PDF points (0 = bottom edge, PDF coordinate system) */ + /** Y coordinate */ y: number; - /** Width in PDF points */ + /** Width */ width: number; - /** Height in PDF points */ + /** Height */ height: number; } -export interface DOMRect { - /** X coordinate in DOM pixels relative to thumbnail container */ - x: number; - /** Y coordinate in DOM pixels relative to thumbnail container */ - y: number; - /** Width in DOM pixels */ - width: number; - /** Height in DOM pixels */ - height: number; +/** Runtime type guard */ +export function isRectangle(value: unknown): value is Rectangle { + if (value === null || typeof value !== "object") return false; + + const r = value as Record; + const isNum = (n: unknown): n is number => + typeof n === "number" && Number.isFinite(n); + + return ( + isNum(r.x) && + isNum(r.y) && + isNum(r.width) && + isNum(r.height) && + r.width >= 0 && + r.height >= 0 + ); } /** @@ -79,9 +86,9 @@ export const calculatePDFBounds = ( * Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin) */ export const domToPDFCoordinates = ( - domRect: DOMRect, + domRect: Rectangle, pdfBounds: PDFBounds -): CropArea => { +): Rectangle => { // Convert DOM coordinates to thumbnail-relative coordinates const thumbX = domRect.x - pdfBounds.offsetX; const thumbY = domRect.y - pdfBounds.offsetY; @@ -104,9 +111,9 @@ export const domToPDFCoordinates = ( * Convert PDF coordinates to DOM coordinates (relative to container) */ export const pdfToDOMCoordinates = ( - cropArea: CropArea, + cropArea: Rectangle, pdfBounds: PDFBounds -): DOMRect => { +): Rectangle => { // Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis) const thumbX = cropArea.x * pdfBounds.scale; const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale; @@ -126,9 +133,9 @@ export const pdfToDOMCoordinates = ( * Constrain a crop area to stay within PDF bounds */ export const constrainCropAreaToPDF = ( - cropArea: CropArea, + cropArea: Rectangle, pdfBounds: PDFBounds -): CropArea => { +): Rectangle => { // Ensure crop area doesn't extend beyond PDF boundaries const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width); const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height); @@ -145,9 +152,9 @@ export const constrainCropAreaToPDF = ( * Constrain DOM coordinates to stay within thumbnail bounds */ export const constrainDOMRectToThumbnail = ( - domRect: DOMRect, + domRect: Rectangle, pdfBounds: PDFBounds -): DOMRect => { +): Rectangle => { const thumbnailLeft = pdfBounds.offsetX; const thumbnailTop = pdfBounds.offsetY; const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth; @@ -189,7 +196,7 @@ export const isPointInThumbnail = ( /** * Create a default crop area that covers the entire PDF */ -export const createFullPDFCropArea = (pdfBounds: PDFBounds): CropArea => { +export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => { return { x: 0, y: 0, @@ -201,7 +208,7 @@ export const createFullPDFCropArea = (pdfBounds: PDFBounds): CropArea => { /** * Round crop coordinates to reasonable precision (0.1 point) */ -export const roundCropArea = (cropArea: CropArea): CropArea => { +export const roundCropArea = (cropArea: Rectangle): Rectangle => { return { x: Math.round(cropArea.x * 10) / 10, y: Math.round(cropArea.y * 10) / 10,