diff --git a/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx b/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx index 6ae4210..36c7336 100644 --- a/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx @@ -1,43 +1,50 @@ import { Box, - Flex, + FormLabel, HStack, - VStack, Icon, - Text, Input, InputGroup, - InputLeftElement, - IconButton, + InputLeftAddon, + Text, useColorModeValue, - Tooltip, -} from "@chakra-ui/react"; -import type * as React from "react"; -import { useState, useEffect } from "react"; -import { RxArrowTopLeft } from "@react-icons/all-files/rx/RxArrowTopLeft"; -import { RxArrowTopRight } from "@react-icons/all-files/rx/RxArrowTopRight"; -import { RxArrowBottomRight } from "@react-icons/all-files/rx/RxArrowBottomRight"; -import { RxArrowBottomLeft } from "@react-icons/all-files/rx/RxArrowBottomLeft"; -import { FieldErrors } from "react-hook-form"; - -type DistorPerspectiveFieldProps = { - name: string; - id?: string; - onChange: (value: PerspectiveObject) => void; - errors?: FieldErrors>; - value?: PerspectiveObject; -}; + VStack, +} from "@chakra-ui/react" +import { RxArrowBottomLeft } from "@react-icons/all-files/rx/RxArrowBottomLeft" +import { RxArrowBottomRight } from "@react-icons/all-files/rx/RxArrowBottomRight" +import { RxArrowTopLeft } from "@react-icons/all-files/rx/RxArrowTopLeft" +import { RxArrowTopRight } from "@react-icons/all-files/rx/RxArrowTopRight" +import type * as React from "react" +import { useEffect, useState } from "react" export type PerspectiveObject = { - x1: string; - y1: string; - x2: string; - y2: string; - x3: string; - y3: string; - x4: string; - y4: string; -}; + x1: string + y1: string + x2: string + y2: string + x3: string + y3: string + x4: string + y4: string +} + +type ErrorObject = { + message: string +} + +type PerspectiveErrors = { + [key in keyof PerspectiveObject]?: ErrorObject +} & ErrorObject + +type AllErrors = Record + +type DistorPerspectiveFieldProps = { + name: string + id?: string + onChange: (value: PerspectiveObject) => void + errors?: AllErrors + value?: PerspectiveObject +} export const DistortPerspectiveInput: React.FC = ({ id, @@ -46,200 +53,120 @@ export const DistortPerspectiveInput: React.FC = ({ name: propertyName, value, }) => { - const [perspective, setPerspective] = useState(value ?? { - x1: "", - y1: "", - x2: "", - y2: "", - x3: "", - y3: "", - x4: "", - y4: "", - }); - const errorRed = useColorModeValue("red.500", "red.300"); - const leftAccessoryBackground = useColorModeValue("gray.100", "gray.700"); + const [perspective, setPerspective] = useState( + value ?? { + x1: "", + y1: "", + x2: "", + y2: "", + x3: "", + y3: "", + x4: "", + y4: "", + }, + ) + const errorRed = useColorModeValue("red.500", "red.300") + const leftAccessoryBackground = useColorModeValue("gray.100", "gray.700") function handleFieldChange(fieldName: string) { return (e: React.ChangeEvent) => { - const val = e.target.value.trim(); + const val = e.target.value.trim() setPerspective((prev) => ({ ...prev, - [fieldName]: val, - })); - }; + [fieldName]: val?.toUpperCase(), + })) + } } useEffect(() => { - onChange(perspective); - }, [perspective]); + onChange(perspective) + }, [perspective]) return ( - - - - - - - - - - - {[ - errors?.[propertyName]?.x1?.message, - errors?.[propertyName]?.y1?.message, - ] - .filter(Boolean) - .join(". ")} - - - - - - - - - - - - - {[ - errors?.[propertyName]?.x2?.message, - errors?.[propertyName]?.y2?.message, - ] - .filter(Boolean) - .join(". ")} - - - - - - - - - - - - - {[ - errors?.[propertyName]?.x3?.message, - errors?.[propertyName]?.y3?.message, - ] - .filter(Boolean) - .join(". ")} - - + + {[ + { + label: "Top left", + name: "topLeft", + icon: RxArrowTopLeft, + x: "x1", + y: "y1", + }, + { + label: "Top right", + name: "topRight", + icon: RxArrowTopRight, + x: "x2", + y: "y2", + }, + { + label: "Bottom right", + name: "bottomRight", + icon: RxArrowBottomRight, + x: "x3", + y: "y3", + }, + { + label: "Bottom left", + name: "bottomLeft", + icon: RxArrowBottomLeft, + x: "x4", + y: "y4", + }, + ].map(({ label, name, icon, x, y }) => ( + + + + + + + {label} corner coordinates + + + + + + {x.toUpperCase()} + + + + {errors?.[propertyName]?.[x as keyof PerspectiveObject ]?.message} + + - - - - - - - - - - {[ - errors?.[propertyName]?.x4?.message, - errors?.[propertyName]?.y4?.message, - ] - .filter(Boolean) - .join(". ")} - - + + + {y.toUpperCase()} + + + + {errors?.[propertyName]?.[y as keyof PerspectiveObject]?.message} + + + + + ))} - ); -}; + ) +} -export default DistortPerspectiveInput; +export default DistortPerspectiveInput diff --git a/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx b/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx index 58ad486..6908fb5 100644 --- a/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx @@ -3,23 +3,22 @@ import { Flex, HStack, Icon, - Text, + IconButton, Input, InputGroup, InputLeftElement, - IconButton, - useColorModeValue, + Text, Tooltip, + useColorModeValue, } from "@chakra-ui/react" -import { set } from "lodash" -import type * as React from "react" -import { useState, useEffect, forwardRef } from "react" +import { LuArrowDownToLine } from "@react-icons/all-files/lu/LuArrowDownToLine" import { LuArrowLeftToLine } from "@react-icons/all-files/lu/LuArrowLeftToLine" import { LuArrowRightToLine } from "@react-icons/all-files/lu/LuArrowRightToLine" import { LuArrowUpToLine } from "@react-icons/all-files/lu/LuArrowUpToLine" -import { LuArrowDownToLine } from "@react-icons/all-files/lu/LuArrowDownToLine" import { TbBoxPadding } from "@react-icons/all-files/tb/TbBoxPadding" -import { FieldErrors } from "react-hook-form" +import { set } from "lodash" +import type * as React from "react" +import { useEffect, useState } from "react" type PaddingMode = "uniform" | "individual" @@ -30,14 +29,6 @@ export type PaddingState = { padding: number | PaddingObject | null | string } -type PaddingInputFieldProps = { - id?: string - onChange: (value: PaddingState) => void - errors?: FieldErrors> - name: string, - value?: Partial -} - export type PaddingObject = { top: number | null right: number | null @@ -45,11 +36,31 @@ export type PaddingObject = { left: number | null } +type ErrorObject = { + message: string +} + +type PaddingErrors = { + [key in keyof PaddingObject]?: ErrorObject +} & ErrorObject + +type AllErrors = Record + +type PaddingInputFieldProps = { + id?: string + onChange: (value: PaddingState) => void + errors?: AllErrors + name: string + value?: Partial +} + function getUpdatedPaddingValue( current: number | PaddingObject | null | string, side: PaddingDirection | "all", value: string, - mode: "uniform" | "individual" + mode: "uniform" | "individual", ): number | PaddingObject | null | string { let inputValue: number | PaddingObject | null | string try { @@ -77,9 +88,15 @@ function getUpdatedPaddingValue( if (typeof inputValue === "number") { commonValue = inputValue } - const updatedPadding = current && typeof current === "object" - ? { ...current } - : { top: commonValue, right: commonValue, bottom: commonValue, left: commonValue } + const updatedPadding = + current && typeof current === "object" + ? { ...current } + : { + top: commonValue, + right: commonValue, + bottom: commonValue, + left: commonValue, + } if (side !== "all") { set(updatedPadding, side, inputValue) } @@ -92,30 +109,35 @@ export const PaddingInputField: React.FC = ({ onChange, errors, name: propertyName, - value + value, }) => { - const [paddingMode, setPaddingMode] = useState(value?.mode ?? "uniform") - const [paddingValue, setPaddingValue] = useState(value?.padding ?? "") + const [paddingMode, setPaddingMode] = useState( + value?.mode ?? "uniform", + ) + const [paddingValue, setPaddingValue] = useState< + number | PaddingObject | null | string + >(value?.padding ?? "") const errorRed = useColorModeValue("red.500", "red.300") const activeColor = useColorModeValue("blue.500", "blue.600") const inactiveColor = useColorModeValue("gray.600", "gray.400") useEffect(() => { - const formatPaddingValue = (value: number | PaddingObject | null | string): string | PaddingObject => { + const formatPaddingValue = ( + value: number | PaddingObject | null | string, + ): string | PaddingObject => { if (value === null) return "" if (typeof value === "number") { return value.toString() } else if (typeof value === "string") { return value } else { - return value; + return value } } const formattedValue = formatPaddingValue(paddingValue) onChange({ mode: paddingMode, padding: formattedValue }) }, [paddingValue, paddingMode]) - return ( = ({ min={0} onChange={(e) => { const val = e.target.value - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - "all", - val, - paddingMode - )) + setPaddingValue( + getUpdatedPaddingValue(paddingValue, "all", val, paddingMode), + ) }} - value={["number", "string"].includes(typeof paddingValue) ? paddingValue : ""} + value={ + ["number", "string"].includes(typeof paddingValue) + ? (paddingValue as string | number) + : "" + } placeholder="Uniform Padding" isInvalid={!!errors?.[propertyName]?.padding} fontSize="sm" /> - {errors?.[propertyName]?.padding?.message} + + {errors?.[propertyName]?.padding?.message} + ) : ( <> @@ -155,7 +180,7 @@ export const PaddingInputField: React.FC = ({ { name: "bottom", label: "Bottom", icon: LuArrowDownToLine }, { name: "left", label: "Left", icon: LuArrowLeftToLine }, ].map(({ name, label, icon }) => ( - + @@ -165,54 +190,79 @@ export const PaddingInputField: React.FC = ({ min={0} onChange={(e) => { const val = e.target.value - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - name as PaddingDirection, - val, - paddingMode - )) + setPaddingValue( + getUpdatedPaddingValue( + paddingValue, + name as PaddingDirection, + val, + paddingMode, + ), + ) }} - value={typeof paddingValue === "object" ? paddingValue?.[name as PaddingDirection] ?? "" : ""} + value={ + typeof paddingValue === "object" + ? (paddingValue?.[name as PaddingDirection] ?? "") + : "" + } placeholder={label} - isInvalid={!!errors?.[propertyName]?.padding?.[name as PaddingDirection]} + isInvalid={ + !!errors?.[propertyName]?.padding?.[ + name as PaddingDirection + ] + } fontSize="sm" /> - {errors?.[propertyName]?.padding?.[name as PaddingDirection]?.message} + + { + errors?.[propertyName]?.padding?.[name as PaddingDirection] + ?.message + } + - )) - } + ))} )} } padding="0.05em" onClick={() => { - const newPaddingMode = paddingMode === "uniform" ? "individual" : "uniform" - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - "all", - JSON.stringify(paddingValue), - newPaddingMode - )) + const newPaddingMode = + paddingMode === "uniform" ? "individual" : "uniform" + setPaddingValue( + getUpdatedPaddingValue( + paddingValue, + "all", + JSON.stringify(paddingValue), + newPaddingMode, + ), + ) setPaddingMode(newPaddingMode) }} variant="outline" diff --git a/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx b/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx index 3738483..9f364e7 100644 --- a/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx @@ -1,17 +1,16 @@ import { + ButtonGroup, HStack, + IconButton, Input, InputGroup, InputRightElement, - IconButton, - ButtonGroup, Text, - useColorModeValue, } from "@chakra-ui/react" -import type * as React from "react" -import { useState, useEffect } from "react" -import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus" import { AiOutlineMinus } from "@react-icons/all-files/ai/AiOutlineMinus" +import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus" +import type * as React from "react" +import { useEffect, useState } from "react" type ZoomInputFieldProps = { id?: string @@ -22,13 +21,12 @@ type ZoomInputFieldProps = { const STEP_SIZE = 10 - /** * Calculate the next zoom value when zooming in * Rounds up to the next step value */ function calculateZoomIn(currentValue: number): number { - return (Math.floor(currentValue / STEP_SIZE) * STEP_SIZE) + STEP_SIZE + return Math.floor(currentValue / STEP_SIZE) * STEP_SIZE + STEP_SIZE } /** @@ -36,7 +34,7 @@ function calculateZoomIn(currentValue: number): number { * Rounds down to the previous step value */ function calculateZoomOut(currentValue: number): number { - return (Math.ceil(currentValue / STEP_SIZE) * STEP_SIZE) - STEP_SIZE + return Math.ceil(currentValue / STEP_SIZE) * STEP_SIZE - STEP_SIZE } export const ZoomInput: React.FC = ({ @@ -46,7 +44,9 @@ export const ZoomInput: React.FC = ({ value, }) => { const [zoomValue, setZoomValue] = useState(value ?? defaultValue) - const [inputValue, setInputValue] = useState((value ?? defaultValue).toString()) + const [inputValue, setInputValue] = useState( + (value ?? defaultValue).toString(), + ) useEffect(() => { onChange(zoomValue) @@ -56,9 +56,9 @@ export const ZoomInput: React.FC = ({ const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value setInputValue(value) - + const numValue = Number(value) - if (!isNaN(numValue) && numValue >= 0) { + if (!Number.isNaN(numValue) && numValue >= 0) { setZoomValue(numValue) } } @@ -87,13 +87,7 @@ export const ZoomInput: React.FC = ({ } return ( - + = ({ onClick={handleZoomIn} /> - ) } diff --git a/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx b/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx index c234e99..846869a 100644 --- a/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx +++ b/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx @@ -445,30 +445,34 @@ export const TransformationConfigSidebar: React.FC = () => { { const raw = watch(field.name) - const n = Number(raw) + const n = Number(String(raw).toUpperCase().replace(/^N/, "-")) + const isNumberWithN = typeof raw === "string" && !Number.isNaN(n) && raw.toUpperCase().startsWith("N") if (!Number.isFinite(n)) return - const { step, min, max } = field.fieldProps ?? {} + const { step, min, max, skipStepCheck } = field.fieldProps ?? {} let v = n if (min !== undefined) v = Math.max(v, min) if (max !== undefined) v = Math.min(v, max) - if (step) { + if (!skipStepCheck && step) { v = Math.round(v / step) * step const dp = (String(step).split(".")[1] || "").length v = Number(v.toFixed(dp)) } - setValue(field.name, String(v)) + const finalValue = v < 0 && isNumberWithN ? `N${Math.abs(v)}` : String(v) + setValue(field.name, finalValue) }} onChange={(e) => { const val = e.target.value + const numSafeVal = String(val).toUpperCase().replace(/^N/, "-") + const isNumberWithN = typeof val === "string" && !Number.isNaN(Number(numSafeVal)) && val.toUpperCase().startsWith("N") if (val === "") { setValue(field.name, "") @@ -486,18 +490,20 @@ export const TransformationConfigSidebar: React.FC = () => { ) { setValue(field.name, "auto") } else if ( + !field.fieldProps?.skipStepCheck && field.fieldProps?.step && !isStepAligned(val, field.fieldProps?.step) ) { return } else if ( field.fieldProps?.min !== undefined && - Number(val) < field.fieldProps.min + Number(numSafeVal) < field.fieldProps.min ) { - setValue(field.name, field.fieldProps.min) + const finalVal = field.fieldProps.min < 0 && isNumberWithN ? `N${Math.abs(field.fieldProps.min)}` : String(field.fieldProps.min) + setValue(field.name, finalVal) } else if ( field.fieldProps?.max !== undefined && - Number(val) > field.fieldProps.max + Number(numSafeVal) > field.fieldProps.max ) { setValue(field.name, field.fieldProps.max) } else { @@ -523,9 +529,9 @@ export const TransformationConfigSidebar: React.FC = () => { max={field.fieldProps?.max || 100} step={field.fieldProps?.step || 1} value={ - Number.isNaN(Number(watch(field.name))) + Number.isNaN(Number(String(watch(field.name)).toUpperCase().replace(/^N/, "-"))) ? 0 - : Number(watch(field.name)) + : Number(String(watch(field.name)).toUpperCase().replace(/^N/, "-")) } defaultValue={field.fieldProps?.defaultValue as number} onChange={(val) => setValue(field.name, val.toString())} @@ -534,7 +540,7 @@ export const TransformationConfigSidebar: React.FC = () => { - + ) : null} diff --git a/packages/imagekit-editor-dev/src/schema/index.ts b/packages/imagekit-editor-dev/src/schema/index.ts index c13c501..1c42a0b 100644 --- a/packages/imagekit-editor-dev/src/schema/index.ts +++ b/packages/imagekit-editor-dev/src/schema/index.ts @@ -11,7 +11,7 @@ import { RxFontItalic } from "@react-icons/all-files/rx/RxFontItalic" import { RxTextAlignCenter } from "@react-icons/all-files/rx/RxTextAlignCenter" import { RxTextAlignLeft } from "@react-icons/all-files/rx/RxTextAlignLeft" import { RxTextAlignRight } from "@react-icons/all-files/rx/RxTextAlignRight" -import { z } from "zod/v3" +import { RefinementCtx, z } from "zod/v3" import { SIMPLE_OVERLAY_TEXT_REGEX, safeBtoa } from "../utils" import { aspectRatioValidator, @@ -26,6 +26,7 @@ import { widthValidator, } from "./transformation" import { GradientPickerState } from "../components/common/GradientPicker" +import { PerspectiveObject } from "../components/common/DistortPerspectiveInput" // Based on ImageKit's supported object list export const DEFAULT_FOCUS_OBJECTS = [ @@ -1441,16 +1442,16 @@ export const transformationSchema: TransformationSchema[] = [ distort: z.coerce.boolean(), distortType: z.enum(["perspective", "arc"]).optional(), distortPerspective: z.object({ - x1: z.union([z.literal(""), z.coerce.number()]), - y1: z.union([z.literal(""), z.coerce.number()]), - x2: z.union([z.literal(""), z.coerce.number()]), - y2: z.union([z.literal(""), z.coerce.number()]), - x3: z.union([z.literal(""), z.coerce.number()]), - y3: z.union([z.literal(""), z.coerce.number()]), - x4: z.union([z.literal(""), z.coerce.number()]), - y4: z.union([z.literal(""), z.coerce.number()]), + x1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), }).optional(), - distortArcDegree: z.coerce.number().min(-359).max(359).optional(), + distortArcDegree: z.string().regex(/^[-N]?\d+$/).optional(), }) .refine( (val) => { @@ -1465,7 +1466,10 @@ export const transformationSchema: TransformationSchema[] = [ message: "At least one value is required", path: [], }, - ), + ) + .superRefine((val, ctx) => { + validatePerspectiveDistort(val, ctx); + }), transformations: [ { label: "Distort", @@ -1513,15 +1517,19 @@ export const transformationSchema: TransformationSchema[] = [ { label: "Distortion Arc Degrees", name: "distortArcDegree", - fieldType: "input", + fieldType: "slider", isTransformation: true, transformationGroup: "distort", isVisible: ({ distort, distortType }) => distort === true && distortType === "arc", helpText: "Enter the arc degree for the arc distortion effect.", - examples: ["15", "30", "45"], + examples: ["15", "30", "-45", "N50"], fieldProps: { - type: "number", - placeholder: "Arc Degrees", + min: -360, + max: 360, + step: 5, + defaultValue: "0", + inputType: "text", + skipStepCheck: true, } } ], @@ -3148,16 +3156,17 @@ export const transformationSchema: TransformationSchema[] = [ distort: z.coerce.boolean(), distortType: z.enum(["perspective", "arc"]).optional(), distortPerspective: z.object({ - x1: z.union([z.literal(""), z.coerce.number()]), - y1: z.union([z.literal(""), z.coerce.number()]), - x2: z.union([z.literal(""), z.coerce.number()]), - y2: z.union([z.literal(""), z.coerce.number()]), - x3: z.union([z.literal(""), z.coerce.number()]), - y3: z.union([z.literal(""), z.coerce.number()]), - x4: z.union([z.literal(""), z.coerce.number()]), - y4: z.union([z.literal(""), z.coerce.number()]), + x1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), }).optional(), - distortArcDegree: z.coerce.number().min(-359).max(359).optional(), + distortArcDegree: z.string().regex(/^[-N]?\d+$/).optional(), + // Radius radius: z.object({ mode: z.enum(["uniform", "individual"]).optional(), @@ -3268,6 +3277,8 @@ export const transformationSchema: TransformationSchema[] = [ } } } + + validatePerspectiveDistort(val, ctx); }), transformations: [ { @@ -3926,15 +3937,19 @@ export const transformationSchema: TransformationSchema[] = [ { label: "Distortion Arc Degrees", name: "distortArcDegree", - fieldType: "input", + fieldType: "slider", isTransformation: true, transformationGroup: "imageLayer", isVisible: ({ distort, distortType }) => distort === true && distortType === "arc", helpText: "Enter the arc degree for the arc distortion effect.", - examples: ["15", "30", "45"], + examples: ["15", "30", "-45", "N50"], fieldProps: { - type: "number", - placeholder: "Arc Degrees", + min: -360, + max: 360, + step: 5, + defaultValue: "0", + inputType: "text", + skipStepCheck: true, } }, ], @@ -4527,8 +4542,8 @@ export const transformationFormatters: Record< const { distortType, distortPerspective, distortArcDegree } = values const distortPrefix = distortType === "perspective" ? "p" : "a" if (distortType === "perspective" && distortPerspective) { - const { x1, y1, x2, y2, x3, y3, x4, y4 } = distortPerspective as Record - const formattedCoords = [x1, y1, x2, y2, x3, y3, x4, y4].map(coord => coord.toString().replace(/^-/, "N")) + const { x1, y1, x2, y2, x3, y3, x4, y4 } = distortPerspective as Record + const formattedCoords = [x1, y1, x2, y2, x3, y3, x4, y4].map(coord => coord.toString().replace(/^-/,"N")) transforms["e-distort"] = `${distortPrefix}-${formattedCoords.join("_")}` } else if (distortType === "arc" && distortArcDegree !== undefined && distortArcDegree !== null) { transforms["e-distort"] = `${distortPrefix}-${distortArcDegree.toString().replace(/^-/, "N")}` @@ -4556,3 +4571,35 @@ export const transformationFormatters: Record< } } } + + +function validatePerspectiveDistort(value: {distortPerspective: PerspectiveObject, distort: boolean, distortType: string} & any, ctx: RefinementCtx) { + const {distort, distortType, distortPerspective} = value; + if (distort && distortType === "perspective" && distortPerspective) { + const perspective: PerspectiveObject = structuredClone(distortPerspective); + let { x1, y1, x2, y2, x3, y3, x4, y4 } = Object.keys(perspective).reduce((acc, key) => { + const value = perspective[key as keyof typeof perspective]; + if (!value) { + acc[key as keyof PerspectiveObject] = value; + } + const numString = value.toUpperCase().replace(/^N/, "-"); + acc[key as keyof PerspectiveObject] = parseInt(numString as string, 10); + return acc; + }, {} as Record); + const allValuesProvided = [x1, y1, x2, y2, x3, y3, x4, y4].every(v => v === 0 || Boolean(v)); + if (allValuesProvided) { + const isTopLeftValid = x1 < x2 && x1 < x3 && y1 < y3 && y1 < y4; + const isTopRightValid = x2 > x1 && x2 > x4 && y2 < y3 && y2 < y4; + const isBottomRightValid = x3 > x4 && x3 > x1 && y3 > y1 && y3 > y2; + const isBottomLeftValid = x4 < x3 && x4 < x2 && y4 > y1 && y4 > y2; + let isValid = isTopLeftValid && isTopRightValid && isBottomRightValid && isBottomLeftValid; + if (!isValid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Perspective coordinates are invalid.", + path: ["distortPerspective"] + }); + } + } + } +} \ No newline at end of file