layouts schick gemacht und packagelimits weiter implementiert

This commit is contained in:
Codex Agent
2025-11-01 22:55:13 +01:00
parent 79b209de9a
commit 8e6c66f0db
16 changed files with 756 additions and 422 deletions

View File

@@ -63,7 +63,7 @@ export function DesignerCanvas({
fabricCanvasRef.current = null;
}
const upperEl = canvas.upperCanvasEl as (HTMLElement & Record<string, unknown>) | undefined;
const upperEl = canvas.upperCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
if (upperEl) {
if (upperEl.__canvas === canvas) {
delete upperEl.__canvas;
@@ -73,7 +73,7 @@ export function DesignerCanvas({
}
}
const lowerEl = canvas.lowerCanvasEl as (HTMLElement & Record<string, unknown>) | undefined;
const lowerEl = canvas.lowerCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
if (lowerEl) {
if (lowerEl.__canvas === canvas) {
delete lowerEl.__canvas;
@@ -140,6 +140,9 @@ export function DesignerCanvas({
selection: !readOnly,
preserveObjectStacking: true,
perPixelTargetFind: true,
transparentCorners: true,
cornerSize: 8,
padding: readOnly ? 0 : 10, // Default padding for text/objects, 0 for readonly
});
fabricCanvasRef.current = canvas;
@@ -149,7 +152,7 @@ export function DesignerCanvas({
(window as unknown as Record<string, unknown>).__inviteCanvas = canvas;
(element as unknown as { __fabricCanvas?: fabric.Canvas }).__fabricCanvas = canvas;
if (containerRef.current) {
const wrapper = containerRef.current as (HTMLElement & Record<string, unknown>);
const wrapper = containerRef.current as unknown as (HTMLElement & Record<string, unknown>);
wrapper.__fabricCanvas = canvas;
Object.defineProperty(wrapper, '__canvas', {
configurable: true,
@@ -214,33 +217,78 @@ export function DesignerCanvas({
onSelect(null);
};
const handleObjectModified = (event: fabric.IEvent<fabric.Object>) => {
const handleObjectModified = (e: any) => {
if (readOnly) {
return;
}
const target = event.target as FabricObjectWithId | undefined;
const target = e.target as FabricObjectWithId | undefined;
if (!target || typeof target.elementId !== 'string') {
return;
}
const elementId = target.elementId;
const bounds = target.getBoundingRect(true, true);
const nextPatch: Partial<LayoutElement> = {
x: clamp(Math.round(bounds.left ?? 0), 0, CANVAS_WIDTH),
y: clamp(Math.round(bounds.top ?? 0), 0, CANVAS_HEIGHT),
width: clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH),
height: clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT),
const bounds = target.getBoundingRect();
let nextPatch: Partial<LayoutElement> = {
x: clamp(Math.round(bounds.left ?? 0), 20, CANVAS_WIDTH - 20),
y: clamp(Math.round(bounds.top ?? 0), 20, CANVAS_HEIGHT - 20),
};
target.set({
scaleX: 1,
scaleY: 1,
left: nextPatch.x,
top: nextPatch.y,
width: nextPatch.width,
height: nextPatch.height,
// Manual collision check: Calculate overlap and push vertically
const otherObjects = canvas.getObjects().filter(obj => obj !== target && (obj as FabricObjectWithId).elementId);
otherObjects.forEach(other => {
const otherBounds = other.getBoundingRect();
const overlapX = Math.max(0, Math.min(bounds.left + bounds.width, otherBounds.left + otherBounds.width) - Math.max(bounds.left, otherBounds.left));
const overlapY = Math.max(0, Math.min(bounds.top + bounds.height, otherBounds.top + otherBounds.height) - Math.max(bounds.top, otherBounds.top));
if (overlapX > 0 && overlapY > 0) {
// Push down by 120px if overlap (massive spacing für größeren QR-Code)
nextPatch.y = Math.max(nextPatch.y, (Number(otherBounds.top || 0)) + (Number(otherBounds.height || 0)) + 120);
}
});
const isImage = target.type === 'image';
if (isImage) {
const currentScaleX = target.scaleX ?? 1;
const currentScaleY = target.scaleY ?? 1;
const naturalWidth = target.width ?? 0;
const naturalHeight = target.height ?? 0;
if (elementId === 'qr') {
// For QR: Enforce uniform scale, cap size, padding=0
const avgScale = (currentScaleX + currentScaleY) / 2;
const cappedSize = Math.min(Math.round(naturalWidth * avgScale), 800); // Cap at 800px for massive QR
nextPatch.width = cappedSize;
nextPatch.height = cappedSize;
nextPatch.scaleX = cappedSize / naturalWidth;
nextPatch.scaleY = cappedSize / naturalHeight;
target.set({
left: nextPatch.x,
top: nextPatch.y,
scaleX: nextPatch.scaleX,
scaleY: nextPatch.scaleY,
padding: 12, // Increased padding for better frame visibility
uniformScaling: true, // Lock aspect ratio
lockScalingFlip: true,
});
} else {
nextPatch.width = Math.round(naturalWidth * currentScaleX);
nextPatch.height = Math.round(naturalHeight * currentScaleY);
nextPatch.scaleX = currentScaleX;
nextPatch.scaleY = currentScaleY;
target.set({ left: nextPatch.x, top: nextPatch.y, padding: 10 });
}
} else {
nextPatch.width = clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH - 40);
nextPatch.height = clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT - 40);
target.set({
scaleX: 1,
scaleY: 1,
left: nextPatch.x,
top: nextPatch.y,
width: nextPatch.width,
height: nextPatch.height,
padding: 10, // Default padding for text
});
}
onChange(elementId, nextPatch);
canvas.requestRenderAll();
};
@@ -348,39 +396,15 @@ export function DesignerCanvas({
const normalizedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
canvas.setZoom(normalizedScale);
const cssWidth = CANVAS_WIDTH * normalizedScale;
const cssHeight = CANVAS_HEIGHT * normalizedScale;
const element = canvas.getElement();
if (element) {
element.style.width = `${cssWidth}px`;
element.style.height = `${cssHeight}px`;
}
if (canvas.upperCanvasEl) {
canvas.upperCanvasEl.style.width = `${cssWidth}px`;
canvas.upperCanvasEl.style.height = `${cssHeight}px`;
}
if (canvas.lowerCanvasEl) {
canvas.lowerCanvasEl.style.width = `${cssWidth}px`;
canvas.lowerCanvasEl.style.height = `${cssHeight}px`;
}
if (canvas.wrapperEl) {
canvas.wrapperEl.style.width = `${cssWidth}px`;
canvas.wrapperEl.style.height = `${cssHeight}px`;
}
if (containerRef.current) {
containerRef.current.style.width = `${cssWidth}px`;
containerRef.current.style.height = `${cssHeight}px`;
}
canvas.calcOffset();
canvas.viewportTransform = [normalizedScale, 0, 0, normalizedScale, 0, 0];
canvas.setDimensions({
width: CANVAS_WIDTH * normalizedScale,
height: CANVAS_HEIGHT * normalizedScale,
});
canvas.requestRenderAll();
canvas.calcViewportBoundaries();
console.log('Zoom applied:', normalizedScale, 'Transform:', canvas.viewportTransform);
}, [scale]);
return (
@@ -472,7 +496,7 @@ export async function renderFabricLayout(
if (typeof object.setCoords === 'function') {
object.setCoords();
}
const bounds = object.getBoundingRect(true, true);
const bounds = object.getBoundingRect();
console.warn('[Invites][Fabric] added object', {
elementId: (object as FabricObjectWithId).elementId,
left: bounds.left,
@@ -495,7 +519,7 @@ export function applyBackground(
color: string,
gradient: { angle?: number; stops?: string[] } | null,
): void {
let background: string | fabric.Gradient = color;
let background: string | fabric.Gradient<'linear'> = color;
if (gradient?.stops?.length) {
const angle = ((gradient.angle ?? 180) * Math.PI) / 180;
@@ -512,15 +536,15 @@ export function applyBackground(
x2: halfWidth + x * halfWidth,
y2: halfHeight + y * halfHeight,
},
colorStops: gradient.stops.map((stop, index) => ({
offset: gradient.stops.length === 1 ? 0 : index / (gradient.stops.length - 1),
colorStops: gradient.stops!.map((stop, index) => ({
offset: gradient.stops!.length === 1 ? 0 : index / (gradient.stops!.length - 1),
color: stop,
})),
});
}
const canvasWithBackgroundFn = canvas as fabric.Canvas & {
setBackgroundColor?: (value: string | fabric.Gradient, callback?: () => void) => void;
setBackgroundColor?: (value: string | fabric.Gradient<'linear'>, callback?: () => void) => void;
};
if (typeof canvasWithBackgroundFn.setBackgroundColor === 'function') {
@@ -578,9 +602,13 @@ export async function createFabricObject({
...baseConfig,
width: element.width,
height: element.height,
fontSize: element.fontSize ?? 26,
fontSize: element.fontSize ?? 36,
fill: textColor,
fontFamily: element.fontFamily ?? 'Lora',
textAlign: mapTextAlign(element.align),
lineHeight: element.lineHeight ?? 1.5,
charSpacing: element.letterSpacing ?? 0.5,
padding: 12, // Enhanced padding for better readability
});
case 'link':
return new fabric.Textbox(element.content ?? '', {
@@ -589,8 +617,12 @@ export async function createFabricObject({
height: element.height,
fontSize: element.fontSize ?? 24,
fill: accentColor,
fontFamily: element.fontFamily ?? 'Montserrat',
underline: true,
textAlign: mapTextAlign(element.align),
lineHeight: element.lineHeight ?? 1.5,
charSpacing: element.letterSpacing ?? 0.5,
padding: 10,
});
case 'badge':
return createTextBadge({
@@ -601,6 +633,8 @@ export async function createFabricObject({
backgroundColor: badgeColor,
textColor: '#ffffff',
fontSize: element.fontSize ?? 22,
lineHeight: element.lineHeight ?? 1.5,
letterSpacing: element.letterSpacing ?? 0.5,
});
case 'cta':
return createTextBadge({
@@ -612,6 +646,8 @@ export async function createFabricObject({
textColor: '#ffffff',
fontSize: element.fontSize ?? 24,
cornerRadius: 18,
lineHeight: element.lineHeight ?? 1.5,
letterSpacing: element.letterSpacing ?? 0.5,
});
case 'logo':
if (logoDataUrl) {
@@ -627,15 +663,28 @@ export async function createFabricObject({
qrCodeDataUrl.length,
qrCodeDataUrl.slice(0, 48),
);
return loadImageObject(qrCodeDataUrl, element, baseConfig, {
const qrImage = await loadImageObject(qrCodeDataUrl, element, baseConfig, {
shadow: 'rgba(15,23,42,0.25)',
padding: 0, // No padding to fix large frame
});
if (qrImage) {
(qrImage as any).uniformScaling = true; // Lock aspect ratio
qrImage.lockScalingFlip = true;
qrImage.padding = 0;
qrImage.cornerColor = 'transparent';
qrImage.borderScaleFactor = 1; // Prevent border inflation on scale
}
console.log('QR DataURL:', qrCodeDataUrl ? 'Loaded' : 'Fallback');
return qrImage;
}
console.log('QR Fallback used - DataURL missing');
return new fabric.Rect({
...baseConfig,
width: element.width,
height: element.height,
fill: secondaryColor,
fill: 'white',
stroke: secondaryColor,
strokeWidth: 2,
rx: 20,
ry: 20,
});
@@ -646,6 +695,7 @@ export async function createFabricObject({
height: element.height,
fontSize: element.fontSize ?? 24,
fill: secondaryColor,
fontFamily: element.fontFamily ?? 'Lora',
textAlign: mapTextAlign(element.align),
});
}
@@ -660,6 +710,8 @@ export function createTextBadge({
textColor,
fontSize,
cornerRadius = 12,
lineHeight = 1.5,
letterSpacing = 0.5,
}: {
baseConfig: FabricObjectWithId;
text: string;
@@ -669,6 +721,8 @@ export function createTextBadge({
textColor: string;
fontSize: number;
cornerRadius?: number;
lineHeight?: number;
letterSpacing?: number;
}): fabric.Group {
const rect = new fabric.Rect({
width,
@@ -688,8 +742,11 @@ export function createTextBadge({
top: height / 2,
fontSize,
fill: textColor,
fontFamily: 'Montserrat',
originY: 'center',
textAlign: 'center',
lineHeight,
charSpacing: letterSpacing,
selectable: false,
evented: false,
});
@@ -707,7 +764,7 @@ export async function loadImageObject(
source: string,
element: LayoutElement,
baseConfig: FabricObjectWithId,
options?: { objectFit?: 'contain' | 'cover'; shadow?: string },
options?: { objectFit?: 'contain' | 'cover'; shadow?: string; padding?: number },
): Promise<fabric.Object | null> {
return new Promise((resolve) => {
let resolved = false;
@@ -741,6 +798,7 @@ export async function loadImageObject(
height: element.height,
scaleX,
scaleY,
padding: options?.padding ?? 0,
});
if (options?.shadow) {
@@ -779,23 +837,18 @@ export async function loadImageObject(
imageElement.onerror = onError;
imageElement.src = source;
} else {
fabric.util.loadImage(
source,
(img) => {
if (!img) {
onError();
return;
}
console.debug('[Invites][Fabric] image loaded', {
source: source.slice(0, 48),
width: (img as HTMLImageElement).width,
height: (img as HTMLImageElement).height,
});
onImageLoaded(img);
},
undefined,
'anonymous',
);
// Use direct Image constructor approach for better compatibility
const img = new Image();
img.onload = () => {
console.debug('[Invites][Fabric] image loaded', {
source: source.slice(0, 48),
width: img.width,
height: img.height,
});
onImageLoaded(img);
};
img.onerror = onError;
img.src = source;
}
} catch (error) {
onError(error);