Skip to content

Commit 8652239

Browse files
Raoul StraczowskiRaoul Straczowski
authored andcommitted
fix scaling bug
1 parent bc5df78 commit 8652239

16 files changed

Lines changed: 126 additions & 63 deletions

src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import { PreviewPanel } from "./components/Preview/PreviewPanel"
1010
import { VectorizedImage } from "./components/Preview/VectorizedImage"
1111
import { useDebouncedSettings } from "./hooks/use-debounced-settings"
1212
import { useVectorize } from "./hooks/use-vectorize"
13-
import { useImageData } from "./store/selectors"
13+
import { useScaledImageData } from "./store/selectors"
1414

1515
export const App = () => {
16-
const imageData = useImageData()
16+
const scaledImageData = useScaledImageData()
1717
const debouncedSettings = useDebouncedSettings(250)
1818
const { vectorize } = useVectorize()
1919

2020
useEffect(() => {
21-
if (imageData) {
21+
if (scaledImageData) {
2222
vectorize()
2323
}
24-
}, [imageData, debouncedSettings, vectorize])
24+
}, [scaledImageData, debouncedSettings, vectorize])
2525

2626
return (
2727
<div className="min-h-screen bg-background font-sans text-text">

src/components/Export/ExportButtonGCode.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { generateGcode } from "../../core/gcode/generate-gcode"
2-
import { useGcodeSettings, usePolylines, useProcessingStatus, useUploadedImage } from "../../store/selectors"
2+
import { useGcodeSettings, useOriginalImage, usePolylines, useScaledImageData, useProcessingStatus } from "../../store/selectors"
33
import { downloadFile, extractFilenameWithoutExtension } from "./utils"
44

55
export const ExportButtonGCode = () => {
66
const polylines = usePolylines()
7-
const uploadedImage = useUploadedImage()
7+
const originalImage = useOriginalImage()
8+
const scaledImageData = useScaledImageData()
89
const processingStatus = useProcessingStatus()
910
const gcodeSettings = useGcodeSettings()
1011

1112
const isDisabled = !polylines || polylines.length === 0 || processingStatus === "processing"
1213

1314
const handleExport = () => {
14-
if (!polylines || !uploadedImage) return
15+
if (!polylines || !originalImage || !scaledImageData) return
1516

1617
const gcode = generateGcode({
1718
polylines,
18-
dimensions: { width: uploadedImage.width, height: uploadedImage.height },
19+
dimensions: { width: scaledImageData.width, height: scaledImageData.height },
1920
gcodeSettings,
2021
})
2122

22-
const filename = extractFilenameWithoutExtension(uploadedImage.file.name)
23+
const filename = extractFilenameWithoutExtension(originalImage.file.name)
2324
downloadFile({ content: gcode, filename: `${filename}.gcode`, mimeType: "text/plain" })
2425
}
2526

src/components/Export/ExportButtonSVG.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { useProcessingStatus, useSvgOutput, useUploadedImage } from "../../store/selectors"
1+
import { useOriginalImage, useProcessingStatus, useSvgOutput } from "../../store/selectors"
22
import { downloadFile, extractFilenameWithoutExtension } from "./utils"
33

44
export const ExportButtonSVG = () => {
55
const svgOutput = useSvgOutput()
6-
const uploadedImage = useUploadedImage()
6+
const originalImage = useOriginalImage()
77
const processingStatus = useProcessingStatus()
88

99
const isDisabled = svgOutput === null || processingStatus === "processing"
1010

1111
const handleDownload = () => {
12-
if (!svgOutput || !uploadedImage) return
12+
if (!svgOutput || !originalImage) return
1313

14-
const filename = extractFilenameWithoutExtension(uploadedImage.file.name)
14+
const filename = extractFilenameWithoutExtension(originalImage.file.name)
1515
downloadFile({ content: svgOutput, filename: `${filename}.svg`, mimeType: "image/svg+xml" })
1616
}
1717

src/components/ImageUploader/ImageUploader.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import clsx from "clsx"
2-
import type { UploadedImage } from "../../core/types"
2+
import type { OriginalImageMetadata } from "../../core/types"
33
import { useDropZone } from "../../hooks/use-drop-zone"
44
import { useFileUpload } from "../../hooks/use-file-upload"
55
import { LoadingIndicator } from "../LoadingIndicator/LoadingIndicator"
66

77
export const ImageUploader = () => {
8-
const { isLoading, error, uploadedImage, fileInputRef, processFile, openFilePicker, handleFileSelect } = useFileUpload()
8+
const { isLoading, error, originalImage, fileInputRef, processFile, openFilePicker, handleFileSelect } = useFileUpload()
99

1010
const { isDragging, dropZoneProps } = useDropZone({
1111
onDrop: processFile,
@@ -21,8 +21,8 @@ export const ImageUploader = () => {
2121
if (isLoading) {
2222
return <LoadingIndicator message="Loading image..." size="sm" className="h-24 justify-center" />
2323
}
24-
if (uploadedImage) {
25-
return <ImagePreview image={uploadedImage} onChangeImage={handleChangeImage} />
24+
if (originalImage) {
25+
return <ImagePreview image={originalImage} onChangeImage={handleChangeImage} />
2626
}
2727
return <DropZone isDragging={isDragging} onClick={openFilePicker} />
2828
}
@@ -56,7 +56,7 @@ const DropZone = ({ isDragging, onClick }: { isDragging: boolean; onClick: () =>
5656
)
5757
}
5858

59-
const ImagePreview = ({ image, onChangeImage }: { image: UploadedImage; onChangeImage: (event: React.MouseEvent) => void }) => {
59+
const ImagePreview = ({ image, onChangeImage }: { image: OriginalImageMetadata; onChangeImage: (event: React.MouseEvent) => void }) => {
6060
return (
6161
<div className="flex items-center gap-4 p-4">
6262
<img src={image.dataUrl} alt="Uploaded preview" className="h-24 w-24 rounded-lg object-cover" />

src/components/Preview/OriginalImage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { useUploadedImage } from "../../store/selectors"
1+
import { useOriginalImage } from "../../store/selectors"
22

33
export const OriginalImage = () => {
4-
const uploadedImage = useUploadedImage()
4+
const originalImage = useOriginalImage()
55

6-
if (!uploadedImage) {
6+
if (!originalImage) {
77
return <Placeholder />
88
}
99

10-
return <img src={uploadedImage.dataUrl} alt="Uploaded image preview" className="max-h-full max-w-full object-contain" />
10+
return <img src={originalImage.dataUrl} alt="Uploaded image preview" className="max-h-full max-w-full object-contain" />
1111
}
1212

1313
const Placeholder = () => {

src/core/image/extract-image-data.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import type { UploadedImage } from "../types.ts"
1+
import type { OriginalImageMetadata } from "../types.ts"
22

3-
const MAX_DIMENSION = 2000
3+
const MAX_DIMENSION = 1024
44

5-
export const extractImageData = async (uploadedImage: UploadedImage): Promise<ImageData> => {
5+
export const scaleImage = async (originalImage: OriginalImageMetadata): Promise<ImageData> => {
66
const { scaledWidth, scaledHeight } = calculateScaledDimensions({
7-
width: uploadedImage.width,
8-
height: uploadedImage.height,
7+
width: originalImage.width,
8+
height: originalImage.height,
99
})
1010

1111
const canvas = createOffscreenCanvas(scaledWidth, scaledHeight)
1212
const context = getCanvasContext(canvas)
1313

14-
await drawImageToCanvas({ context, dataUrl: uploadedImage.dataUrl, scaledWidth, scaledHeight })
14+
await drawImageToCanvas({ context, dataUrl: originalImage.dataUrl, scaledWidth, scaledHeight })
1515

1616
return context.getImageData(0, 0, scaledWidth, scaledHeight)
1717
}

src/core/image/load-image.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("loadImage", () => {
5454
globalThis.Image = originalImage
5555
})
5656

57-
it("should load PNG image and return UploadedImage", async () => {
57+
it("should load PNG image and return OriginalImageMetadata", async () => {
5858
const mockDataUrl = "data:image/png;base64,abc123"
5959
globalThis.FileReader = createMockFileReader(mockDataUrl) as unknown as typeof FileReader
6060
globalThis.Image = createMockImage(100, 200) as unknown as typeof Image
@@ -69,7 +69,7 @@ describe("loadImage", () => {
6969
assert.strictEqual(result.height, 200)
7070
})
7171

72-
it("should load JPG image and return UploadedImage", async () => {
72+
it("should load JPG image and return OriginalImageMetadata", async () => {
7373
const mockDataUrl = "data:image/jpeg;base64,xyz789"
7474
globalThis.FileReader = createMockFileReader(mockDataUrl) as unknown as typeof FileReader
7575
globalThis.Image = createMockImage(800, 600) as unknown as typeof Image

src/core/image/load-image.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { UploadedImage } from "../types.ts"
1+
import type { OriginalImageMetadata } from "../types.ts"
22

3-
export const loadImage = (file: File): Promise<UploadedImage> => {
3+
export const loadImage = (file: File): Promise<OriginalImageMetadata> => {
44
return readFileAsDataUrl(file).then((dataUrl) => loadImageElement({ file, dataUrl }))
55
}
66

@@ -25,7 +25,7 @@ const readFileAsDataUrl = (file: File): Promise<string> => {
2525
})
2626
}
2727

28-
const loadImageElement = ({ file, dataUrl }: { file: File; dataUrl: string }): Promise<UploadedImage> => {
28+
const loadImageElement = ({ file, dataUrl }: { file: File; dataUrl: string }): Promise<OriginalImageMetadata> => {
2929
return new Promise((resolve, reject) => {
3030
const image = new Image()
3131

src/core/image/scale-image.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { OriginalImageMetadata } from "../types.ts"
2+
3+
const MAX_DIMENSION = 2000
4+
5+
export const scaleImage = async (originalImage: OriginalImageMetadata): Promise<ImageData> => {
6+
const { scaledWidth, scaledHeight } = calculateScaledDimensions({
7+
width: originalImage.width,
8+
height: originalImage.height,
9+
})
10+
11+
const canvas = createOffscreenCanvas(scaledWidth, scaledHeight)
12+
const context = getCanvasContext(canvas)
13+
14+
await drawImageToCanvas({ context, dataUrl: originalImage.dataUrl, scaledWidth, scaledHeight })
15+
16+
return context.getImageData(0, 0, scaledWidth, scaledHeight)
17+
}
18+
19+
const calculateScaledDimensions = ({ width, height }: { width: number; height: number }): { scaledWidth: number; scaledHeight: number } => {
20+
if (width <= MAX_DIMENSION && height <= MAX_DIMENSION) {
21+
return { scaledWidth: width, scaledHeight: height }
22+
}
23+
24+
const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height)
25+
26+
return {
27+
scaledWidth: Math.round(width * scale),
28+
scaledHeight: Math.round(height * scale),
29+
}
30+
}
31+
32+
const createOffscreenCanvas = (width: number, height: number): HTMLCanvasElement => {
33+
const canvas = document.createElement("canvas")
34+
canvas.width = width
35+
canvas.height = height
36+
return canvas
37+
}
38+
39+
const getCanvasContext = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
40+
const context = canvas.getContext("2d")
41+
if (!context) {
42+
throw new Error("Failed to get canvas 2D context")
43+
}
44+
return context
45+
}
46+
47+
const drawImageToCanvas = ({ context, dataUrl, scaledWidth, scaledHeight }: { context: CanvasRenderingContext2D; dataUrl: string; scaledWidth: number; scaledHeight: number }): Promise<void> => {
48+
return new Promise((resolve, reject) => {
49+
const image = new Image()
50+
51+
image.onload = () => {
52+
context.drawImage(image, 0, 0, scaledWidth, scaledHeight)
53+
resolve()
54+
}
55+
56+
image.onerror = () => {
57+
reject(new Error("Failed to load image for extraction"))
58+
}
59+
60+
image.src = dataUrl
61+
})
62+
}

src/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type ImageData = {
2929
colorSpace: PredefinedColorSpace
3030
}
3131

32-
export type UploadedImage = {
32+
export type OriginalImageMetadata = {
3333
file: File
3434
dataUrl: string
3535
width: number

0 commit comments

Comments
 (0)