1
0
mirror of https://github.com/MultiMote/niimblue synced 2026-01-19 19:37:11 +03:00

Add image fit modes (closes #68)

This commit is contained in:
MultiMote
2025-01-24 11:56:01 +03:00
parent 900191daec
commit 29966cd8a2
7 changed files with 100 additions and 13 deletions

View File

@@ -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"
}

View File

@@ -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 }));
};
</script>
<button class="btn btn-sm btn-secondary" on:click={putToCenterV} title={$tr("params.generic.center.vertical")}>
@@ -34,7 +59,18 @@
<MdIcon icon="horizontal_distribute" />
</button>
{#if selectedObject instanceof fabric.FabricImage}
<button class="btn btn-sm btn-secondary" on:click={fit} title={$tr("params.generic.fit")}>
<MdIcon icon="fit_screen" />
</button>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-secondary" on:click={fit} title={$tr("params.generic.fit")}>
<MdIcon icon="fit_screen" />
</button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split px-1" data-bs-toggle="dropdown"
></button>
<div class="dropdown-menu p-1">
<select class="form-select form-select-sm" value={$appConfig.fitMode ?? "stretch"} on:change={fitModeChanged}>
<option value="stretch">{$tr("params.generic.fit.mode.stretch")}</option>
<option value="ratio_min">{$tr("params.generic.fit.mode.ratio_min")}</option>
<option value="ratio_max">{$tr("params.generic.fit.mode.ratio_max")}</option>
</select>
</div>
</div>
{/if}

View File

@@ -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",

View File

@@ -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": "Слева",

View File

@@ -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<string[]>([OBJECT_DEFAULTS_TEXT.fontFamily]);
export const appConfig = writablePersisted<AppConfig>("config", AppConfigSchema, APP_CONFIG_DEFAULTS);
export const connectionState = writable<ConnectionState>("disconnected");
export const connectedPrinterName = writable<string>("");

View File

@@ -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<typeof LabelPropsSchema>;
export type LabelPreset = z.infer<typeof LabelPresetSchema>;
export type FabricJson = z.infer<typeof FabricJsonSchema>;
@@ -93,3 +98,4 @@ export type ExportedLabelTemplate = z.infer<typeof ExportedLabelTemplateSchema>;
export type PreviewPropsOffset = z.infer<typeof PreviewPropsOffsetSchema>;
export type PreviewProps = z.infer<typeof PreviewPropsSchema>;
export type AutomationProps = z.infer<typeof AutomationPropsSchema>;
export type AppConfig = z.infer<typeof AppConfigSchema>;

View File

@@ -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<T>(key: string, schema: z.ZodType<T>, initialValue: T): Writable<T> {
const wr = writable<T>(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<T>) => {
const newValue: T = updater(get(wr));
LocalStoragePersistence.validateAndSaveObject(key, newValue, schema);
wr.set(newValue);
},
};
}
export class LocalStoragePersistence {
/** Result in kilobytes */