From 29966cd8a2a3e28504807e6d63aacde10dcae705 Mon Sep 17 00:00:00 2001 From: MultiMote Date: Fri, 24 Jan 2025 11:56:01 +0300 Subject: [PATCH] Add image fit modes (closes #68) --- src/defaults.ts | 6 ++- src/lib/GenericObjectParamsControls.svelte | 54 ++++++++++++++++++---- src/locale/dicts/en.json | 3 ++ src/locale/dicts/ru.json | 3 ++ src/stores.ts | 7 +-- src/types.ts | 6 +++ src/utils/persistence.ts | 34 ++++++++++++++ 7 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/defaults.ts b/src/defaults.ts index 5953170..35370ed 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -1,5 +1,5 @@ import * as fabric from "fabric"; -import type { LabelPreset, LabelProps } from "./types"; +import type { AppConfig, LabelPreset, LabelProps } from "./types"; /** Default presets for LabelPropsEditor */ export const DEFAULT_LABEL_PRESETS: LabelPreset[] = [ @@ -57,3 +57,7 @@ export const THUMBNAIL_HEIGHT = 48; /** Generate thumbnail in jpeg format with this quality */ export const THUMBNAIL_QUALITY = 0.7; + +export const APP_CONFIG_DEFAULTS: AppConfig = { + fitMode: "stretch" +} diff --git a/src/lib/GenericObjectParamsControls.svelte b/src/lib/GenericObjectParamsControls.svelte index bd521c9..2306a93 100644 --- a/src/lib/GenericObjectParamsControls.svelte +++ b/src/lib/GenericObjectParamsControls.svelte @@ -2,6 +2,7 @@ import * as fabric from "fabric"; import { tr } from "../utils/i18n"; import MdIcon from "./basic/MdIcon.svelte"; + import { appConfig } from "../stores"; export let selectedObject: fabric.FabricObject; export let valueUpdated: () => void; @@ -17,14 +18,38 @@ }; const fit = () => { - selectedObject.set({ - left: 0, - top: 0, - scaleX: selectedObject.canvas!.width / selectedObject.width, - scaleY: selectedObject.canvas!.height / selectedObject.height, - }); + const imageRatio = selectedObject.width / selectedObject.height; + const canvasRatio = selectedObject.canvas!.width / selectedObject.canvas!.height; + + if ($appConfig.fitMode === "ratio_min") { + if (imageRatio > canvasRatio) { + selectedObject.scaleToWidth(selectedObject.canvas!.width); + } else { + selectedObject.scaleToHeight(selectedObject.canvas!.height); + } + selectedObject.canvas!.centerObject(selectedObject); + } else if ($appConfig.fitMode === "ratio_max") { + if (imageRatio > canvasRatio) { + selectedObject.scaleToHeight(selectedObject.canvas!.height); + } else { + selectedObject.scaleToWidth(selectedObject.canvas!.width); + } + selectedObject.canvas!.centerObject(selectedObject); + } else { + selectedObject.set({ + left: 0, + top: 0, + scaleX: selectedObject.canvas!.width / selectedObject.width, + scaleY: selectedObject.canvas!.height / selectedObject.height, + }); + } valueUpdated(); }; + + const fitModeChanged = (e: Event & { currentTarget: HTMLSelectElement }) => { + const fitMode = e.currentTarget.value as "stretch" | "ratio_min" | "ratio_max"; + appConfig.update((v) => ({ ...v, fitMode: fitMode })); + }; {#if selectedObject instanceof fabric.FabricImage} - +
+ + + +
{/if} diff --git a/src/locale/dicts/en.json b/src/locale/dicts/en.json index 1d7c3ef..63f331c 100644 --- a/src/locale/dicts/en.json +++ b/src/locale/dicts/en.json @@ -41,6 +41,9 @@ "params.generic.center.horizontal": "Center horizontally", "params.generic.center.vertical": "Center vertically", "params.generic.fit": "Fit to page", + "params.generic.fit.mode.stretch": "Stretch", + "params.generic.fit.mode.ratio_min": "Keep ratio #1", + "params.generic.fit.mode.ratio_max": "Keep ratio #2", "params.label.apply": "Apply", "params.label.current": "Current parameters:", "params.label.direction.left": "Left", diff --git a/src/locale/dicts/ru.json b/src/locale/dicts/ru.json index 3c36ec5..e45ff66 100644 --- a/src/locale/dicts/ru.json +++ b/src/locale/dicts/ru.json @@ -41,6 +41,9 @@ "params.generic.center.horizontal": "Выровнять горизонтально", "params.generic.center.vertical": "Выровнять вертикально", "params.generic.fit": "Растянуть под размер страницы", + "params.generic.fit.mode.stretch": "Растянуть", + "params.generic.fit.mode.ratio_min": "Сохранить соотношение сторон #1", + "params.generic.fit.mode.ratio_max": "Сохранить соотношение сторон #2", "params.label.apply": "Применить", "params.label.current": "Текущие параметры:", "params.label.direction.left": "Слева", diff --git a/src/stores.ts b/src/stores.ts index 2a141aa..8e77524 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -1,5 +1,5 @@ import { get, readable, writable } from "svelte/store"; -import type { AutomationProps, ConnectionState, ConnectionType } from "./types"; +import { AppConfigSchema, type AppConfig, type AutomationProps, type ConnectionState, type ConnectionType } from "./types"; import { NiimbotBluetoothClient, NiimbotCapacitorBleClient, @@ -16,10 +16,11 @@ import { } from "@mmote/niimbluelib"; import { Toasts } from "./utils/toasts"; import { tr } from "./utils/i18n"; -import { LocalStoragePersistence } from "./utils/persistence"; -import { OBJECT_DEFAULTS_TEXT } from "./defaults"; +import { LocalStoragePersistence, writablePersisted } from "./utils/persistence"; +import { APP_CONFIG_DEFAULTS, OBJECT_DEFAULTS_TEXT } from "./defaults"; export const fontCache = writable([OBJECT_DEFAULTS_TEXT.fontFamily]); +export const appConfig = writablePersisted("config", AppConfigSchema, APP_CONFIG_DEFAULTS); export const connectionState = writable("disconnected"); export const connectedPrinterName = writable(""); diff --git a/src/types.ts b/src/types.ts index a27cf7f..db5e695 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,6 +86,11 @@ export const AutomationPropsSchema = z.object({ startPrint: z.enum(["after_connect", "immediately"]).optional(), }); +export const AppConfigSchema = z.object({ + /** Keep image aspect ration when using "fit" button */ + fitMode: z.enum(["stretch", "ratio_min", "ratio_max"]).optional(), +}); + export type LabelProps = z.infer; export type LabelPreset = z.infer; export type FabricJson = z.infer; @@ -93,3 +98,4 @@ export type ExportedLabelTemplate = z.infer; export type PreviewPropsOffset = z.infer; export type PreviewProps = z.infer; export type AutomationProps = z.infer; +export type AppConfig = z.infer; diff --git a/src/utils/persistence.ts b/src/utils/persistence.ts index 65923bd..3279e08 100644 --- a/src/utils/persistence.ts +++ b/src/utils/persistence.ts @@ -14,6 +14,40 @@ import { } from "../types"; import { z } from "zod"; import { FileUtils } from "./file_utils"; +import { get, writable, type Updater, type Writable } from "svelte/store"; + +/** Writable store, value is persisted to localStorage */ +export function writablePersisted(key: string, schema: z.ZodType, initialValue: T): Writable { + const wr = writable(initialValue); + + console.log("read"); + + try { + const val = LocalStoragePersistence.loadAndValidateObject(key, schema); + if (val != null) { + wr.set(val); + } else { + wr.set(initialValue); + } + } catch (_e) { + wr.set(initialValue); + } + + return { + subscribe: wr.subscribe, + + set: (value: T) => { + LocalStoragePersistence.validateAndSaveObject(key, value, schema); + wr.set(value); + }, + + update: (updater: Updater) => { + const newValue: T = updater(get(wr)); + LocalStoragePersistence.validateAndSaveObject(key, newValue, schema); + wr.set(newValue); + }, + }; +} export class LocalStoragePersistence { /** Result in kilobytes */