diff --git a/src/components/ImageUpload.vue b/src/components/ImageUpload.vue index 94cdf6e..11b1426 100644 --- a/src/components/ImageUpload.vue +++ b/src/components/ImageUpload.vue @@ -1,8 +1,10 @@ @@ -132,7 +166,193 @@ function onFileSelect(e: Event) { > {{ error }}

- - + + +
+ +
+ +
+

Example

+
+
+ Before: angled photograph of a Pioneer CDJ-1000MK3 top case +

Before — angled shot

+
+
+ After: perspective-corrected front-facing view +

After — corrected perspective

+
+
+
+ +
+

+ Correct perspective distortion in photographs using + known reference dimensions. Useful when you need to + use a photo as a scale reference for design work, + measure objects from photographs, or recover accurate + geometry from angled shots. +

+
+ +
+

How it works

+

+ Place an object with known dimensions (a ruler, credit card, or A4 sheet) next to the subject + you want to photograph. Take the picture from any angle. Skwik uses the reference object to + compute a perspective transform and produce a corrected, front-facing image with accurate + proportions. +

+

+ This is especially handy for reverse-engineering enclosure cutouts, measuring parts you + can't easily reach, or getting a dimensionally accurate top-down view without a tripod. +

+ +

Tips for best results

+ + + +
+ + + + + + + + ref + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wide angle + (more distortion) + zoomed in + + + + + + + higher is better + +
+ +
+ diff --git a/src/lib/datum-cache.ts b/src/lib/datum-cache.ts new file mode 100644 index 0000000..2694604 --- /dev/null +++ b/src/lib/datum-cache.ts @@ -0,0 +1,48 @@ +import type { Datum } from "@/types" + +const KEY_PREFIX = "skwik-datums-" + +export function saveDatums(hash: string, datums: Datum[]): void { + try { + localStorage.setItem( + KEY_PREFIX + hash, + JSON.stringify(datums), + ) + } catch { + // localStorage full or unavailable — silently ignore + } +} + +export function loadDatums(hash: string): Datum[] | null { + try { + const raw = localStorage.getItem(KEY_PREFIX + hash) + if (!raw) return null + return JSON.parse(raw) as Datum[] + } catch { + return null + } +} + +export function clearCache(): void { + const toRemove: string[] = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key?.startsWith(KEY_PREFIX)) { + toRemove.push(key) + } + } + for (const key of toRemove) { + localStorage.removeItem(key) + } +} + +export function getCacheSize(): number { + let count = 0 + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key?.startsWith(KEY_PREFIX)) { + count++ + } + } + return count +} diff --git a/src/lib/file-hash.ts b/src/lib/file-hash.ts new file mode 100644 index 0000000..43178e9 --- /dev/null +++ b/src/lib/file-hash.ts @@ -0,0 +1,6 @@ +export async function hashFile(file: File): Promise { + const buffer = await file.arrayBuffer() + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("") +} diff --git a/src/lib/settings-cache.ts b/src/lib/settings-cache.ts new file mode 100644 index 0000000..403651d --- /dev/null +++ b/src/lib/settings-cache.ts @@ -0,0 +1,24 @@ +export interface SkwikSettings { + scalePxPerMm: number + includeScaleBar: boolean +} + +const KEY = "skwik-settings" + +export function saveSettings(settings: SkwikSettings): void { + try { + localStorage.setItem(KEY, JSON.stringify(settings)) + } catch { + // localStorage full or unavailable — silently ignore + } +} + +export function loadSettings(): SkwikSettings | null { + try { + const raw = localStorage.getItem(KEY) + if (!raw) return null + return JSON.parse(raw) as SkwikSettings + } catch { + return null + } +} diff --git a/src/stores/app.ts b/src/stores/app.ts index 82872c2..269f2d3 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -2,9 +2,13 @@ import { defineStore } from "pinia" import { ref, computed } from "vue" import type { AppStep, Datum, DeskewResult, ExifData } from "@/types" import { DEFAULT_SCALE_PX_PER_MM } from "@/types" +import { loadSettings } from "@/lib/settings-cache" export const useAppStore = defineStore("app", () => { + const cached = loadSettings() + const currentStep = ref(1) + const maxStepReached = ref(1) const originalFile = ref(null) const loadedImage = ref(null) const exifData = ref({}) @@ -13,7 +17,11 @@ export const useAppStore = defineStore("app", () => { const isProcessing = ref(false) const processingStatus = ref("") const selectedDatumId = ref(null) - const scalePxPerMm = ref(DEFAULT_SCALE_PX_PER_MM) + const scalePxPerMm = ref( + cached?.scalePxPerMm ?? DEFAULT_SCALE_PX_PER_MM, + ) + const fileHash = ref(null) + const cacheRestoreMessage = ref("") const canProceedToStep2 = computed(() => loadedImage.value !== null) const canProceedToStep3 = computed(() => canProceedToStep2.value) @@ -36,6 +44,9 @@ export const useAppStore = defineStore("app", () => { function goToStep(step: AppStep) { currentStep.value = step + if (step > maxStepReached.value) { + maxStepReached.value = step + } } function addDatum(datum: Datum) { @@ -65,8 +76,13 @@ export const useAppStore = defineStore("app", () => { deskewResult.value = result } + function setFileHash(hash: string) { + fileHash.value = hash + } + function reset() { currentStep.value = 1 + maxStepReached.value = 1 originalFile.value = null loadedImage.value = null exifData.value = {} @@ -76,10 +92,13 @@ export const useAppStore = defineStore("app", () => { processingStatus.value = "" selectedDatumId.value = null scalePxPerMm.value = DEFAULT_SCALE_PX_PER_MM + fileHash.value = null + cacheRestoreMessage.value = "" } return { currentStep, + maxStepReached, originalFile, loadedImage, exifData, @@ -89,6 +108,8 @@ export const useAppStore = defineStore("app", () => { processingStatus, selectedDatumId, scalePxPerMm, + fileHash, + cacheRestoreMessage, canProceedToStep2, canProceedToStep3, canProceedToStep4, @@ -99,6 +120,7 @@ export const useAppStore = defineStore("app", () => { updateDatum, removeDatum, setResult, + setFileHash, reset, } })