Enhance canvas functionality by adding media preview capabilities and image upload handling. Introduce compressed image previews during uploads, improve media library integration, and implement retry logic for bridge edge creation. Update dashboard to display media previews and optimize image node handling.

This commit is contained in:
Matthias
2026-04-08 20:44:31 +02:00
parent a7eb2bc99c
commit b7f24223f2
43 changed files with 4064 additions and 148 deletions

View File

@@ -0,0 +1,166 @@
export type CropResizeMode = "source" | "custom";
export type CropFitMode = "cover" | "contain" | "fill";
export type CropRect = {
x: number;
y: number;
width: number;
height: number;
};
export type CropResizeSettings = {
mode: CropResizeMode;
width?: number;
height?: number;
fit: CropFitMode;
keepAspect: boolean;
};
export type CropNodeData = {
crop: CropRect;
resize: CropResizeSettings;
};
const CROP_MIN_SIZE = 0.01;
const CUSTOM_SIZE_MIN = 1;
const CUSTOM_SIZE_MAX = 16_384;
const DEFAULT_CUSTOM_SIZE = 1024;
const DISALLOWED_CROP_PAYLOAD_KEYS = [
"blob",
"blobUrl",
"imageData",
"storageId",
"url",
] as const;
export const DEFAULT_CROP_NODE_DATA: CropNodeData = {
crop: {
x: 0,
y: 0,
width: 1,
height: 1,
},
resize: {
mode: "source",
fit: "cover",
keepAspect: true,
},
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readFiniteNumber(value: unknown): number | null {
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
}
return value;
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function clampUnit(value: number | null, fallback: number): number {
if (value === null) {
return fallback;
}
return clamp(value, 0, 1);
}
function normalizeCropRect(value: unknown): CropRect {
const source = isRecord(value) ? value : {};
const base = DEFAULT_CROP_NODE_DATA.crop;
const xInput = readFiniteNumber(source.x);
const yInput = readFiniteNumber(source.y);
const widthInput = readFiniteNumber(source.width);
const heightInput = readFiniteNumber(source.height);
const width = widthInput !== null && widthInput > 0
? clamp(widthInput, CROP_MIN_SIZE, 1)
: base.width;
const height = heightInput !== null && heightInput > 0
? clamp(heightInput, CROP_MIN_SIZE, 1)
: base.height;
const x = clamp(clampUnit(xInput, base.x), 0, Math.max(0, 1 - width));
const y = clamp(clampUnit(yInput, base.y), 0, Math.max(0, 1 - height));
return {
x,
y,
width,
height,
};
}
function normalizeCustomSize(value: unknown): number | undefined {
if (!Number.isInteger(value)) {
return undefined;
}
const parsed = value as number;
if (parsed < CUSTOM_SIZE_MIN || parsed > CUSTOM_SIZE_MAX) {
return undefined;
}
return parsed;
}
function normalizeResizeSettings(value: unknown): CropResizeSettings {
const source = isRecord(value) ? value : {};
const defaults = DEFAULT_CROP_NODE_DATA.resize;
const mode: CropResizeMode = source.mode === "custom" ? "custom" : defaults.mode;
const fit: CropFitMode =
source.fit === "contain" || source.fit === "fill" || source.fit === "cover"
? source.fit
: defaults.fit;
const keepAspect = typeof source.keepAspect === "boolean" ? source.keepAspect : defaults.keepAspect;
if (mode !== "custom") {
return {
mode,
fit,
keepAspect,
};
}
return {
mode,
width: normalizeCustomSize(source.width) ?? DEFAULT_CUSTOM_SIZE,
height: normalizeCustomSize(source.height) ?? DEFAULT_CUSTOM_SIZE,
fit,
keepAspect,
};
}
function assertNoDisallowedPayloadFields(data: Record<string, unknown>): void {
for (const key of DISALLOWED_CROP_PAYLOAD_KEYS) {
if (key in data) {
throw new Error(`Crop node accepts parameter data only. '${key}' is not allowed in data.`);
}
}
}
export function normalizeCropNodeData(
value: unknown,
options?: {
rejectDisallowedPayloadFields?: boolean;
},
): CropNodeData {
const source = isRecord(value) ? value : {};
if (options?.rejectDisallowedPayloadFields) {
assertNoDisallowedPayloadFields(source);
}
return {
crop: normalizeCropRect(source.crop),
resize: normalizeResizeSettings(source.resize),
};
}