@merchi/product-editor 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -0
- package/README.md +174 -0
- package/dist/components/AlignMenu.d.ts +3 -0
- package/dist/components/AlignMenu.js +61 -0
- package/dist/components/FloatingToolbar.d.ts +4 -0
- package/dist/components/FloatingToolbar.js +97 -0
- package/dist/components/FormatButtons.d.ts +2 -0
- package/dist/components/FormatButtons.js +22 -0
- package/dist/components/ImageNavButton.d.ts +7 -0
- package/dist/components/ImageNavButton.js +14 -0
- package/dist/components/ImageZoomModal.d.ts +12 -0
- package/dist/components/ImageZoomModal.js +129 -0
- package/dist/components/LayerPanel.d.ts +4 -0
- package/dist/components/LayerPanel.js +223 -0
- package/dist/components/LoadingOverlay.d.ts +9 -0
- package/dist/components/LoadingOverlay.js +17 -0
- package/dist/components/ProductEditor.d.ts +4 -0
- package/dist/components/ProductEditor.js +67 -0
- package/dist/components/ProductImageGallery.d.ts +6 -0
- package/dist/components/ProductImageGallery.js +148 -0
- package/dist/components/ProductPreviews.d.ts +7 -0
- package/dist/components/ProductPreviews.js +281 -0
- package/dist/components/TextToolbar.d.ts +4 -0
- package/dist/components/TextToolbar.js +117 -0
- package/dist/components/Toolbar.d.ts +3 -0
- package/dist/components/Toolbar.js +47 -0
- package/dist/config/colorConfig.d.ts +1 -0
- package/dist/config/colorConfig.js +10 -0
- package/dist/config/fontConfig.d.ts +5 -0
- package/dist/config/fontConfig.js +21 -0
- package/dist/context/ProductEditorContext.d.ts +60 -0
- package/dist/context/ProductEditorContext.js +743 -0
- package/dist/hooks/useSwipeNavigate.d.ts +11 -0
- package/dist/hooks/useSwipeNavigate.js +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -0
- package/dist/styles/BottomPreviewDisplay.css +65 -0
- package/dist/styles/FloatingToolbar.css +41 -0
- package/dist/styles/ImageGallery.css +147 -0
- package/dist/styles/ImageNavButton.css +35 -0
- package/dist/styles/ImageZoomModal.css +101 -0
- package/dist/styles/LayerPanel.css +179 -0
- package/dist/styles/LoadingOverlay.css +48 -0
- package/dist/styles/ProductEditor.css +192 -0
- package/dist/styles/TextToolbar.css +327 -0
- package/dist/styles/Toolbar.css +125 -0
- package/dist/styles/index.css +5 -0
- package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
- package/dist/styles/textToolbarSelectStyles.js +30 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.js +17 -0
- package/dist/utils/canvasAddText.d.ts +11 -0
- package/dist/utils/canvasAddText.js +66 -0
- package/dist/utils/canvasUtils.d.ts +37 -0
- package/dist/utils/canvasUtils.js +342 -0
- package/dist/utils/deviceUtils.d.ts +5 -0
- package/dist/utils/deviceUtils.js +33 -0
- package/dist/utils/draftTemplateUtils.d.ts +23 -0
- package/dist/utils/draftTemplateUtils.js +66 -0
- package/dist/utils/grid.d.ts +32 -0
- package/dist/utils/grid.js +151 -0
- package/dist/utils/imageUtils.d.ts +12 -0
- package/dist/utils/imageUtils.js +178 -0
- package/dist/utils/job.d.ts +40 -0
- package/dist/utils/job.js +115 -0
- package/dist/utils/keyboard.d.ts +1 -0
- package/dist/utils/keyboard.js +26 -0
- package/dist/utils/previewUtils.d.ts +9 -0
- package/dist/utils/previewUtils.js +39 -0
- package/dist/utils/psdConverter.d.ts +40 -0
- package/dist/utils/psdConverter.js +516 -0
- package/dist/utils/psdRenderUtils.d.ts +12 -0
- package/dist/utils/psdRenderUtils.js +465 -0
- package/dist/utils/renderUtils.d.ts +2 -0
- package/dist/utils/renderUtils.js +85 -0
- package/package.json +66 -0
- package/src/components/AlignMenu.tsx +49 -0
- package/src/components/FloatingToolbar.tsx +83 -0
- package/src/components/FormatButtons.tsx +31 -0
- package/src/components/ImageNavButton.tsx +23 -0
- package/src/components/ImageZoomModal.tsx +163 -0
- package/src/components/LayerPanel.tsx +256 -0
- package/src/components/LoadingOverlay.tsx +27 -0
- package/src/components/ProductEditor.tsx +104 -0
- package/src/components/ProductImageGallery.tsx +181 -0
- package/src/components/ProductPreviews.tsx +243 -0
- package/src/components/TextToolbar.tsx +164 -0
- package/src/components/Toolbar.tsx +86 -0
- package/src/config/colorConfig.ts +7 -0
- package/src/config/fontConfig.ts +23 -0
- package/src/context/ProductEditorContext.tsx +829 -0
- package/src/hooks/useSwipeNavigate.ts +40 -0
- package/src/index.ts +6 -0
- package/src/styles/BottomPreviewDisplay.css +65 -0
- package/src/styles/FloatingToolbar.css +41 -0
- package/src/styles/ImageGallery.css +147 -0
- package/src/styles/ImageNavButton.css +35 -0
- package/src/styles/ImageZoomModal.css +101 -0
- package/src/styles/LayerPanel.css +179 -0
- package/src/styles/LoadingOverlay.css +48 -0
- package/src/styles/ProductEditor.css +192 -0
- package/src/styles/TextToolbar.css +327 -0
- package/src/styles/Toolbar.css +125 -0
- package/src/styles/index.css +5 -0
- package/src/styles/textToolbarSelectStyles.ts +60 -0
- package/src/types/ag-psd/index.d.ts +1 -0
- package/src/types/fabric.d.ts +14 -0
- package/src/types/lodash/index.d.ts +1 -0
- package/src/types/psd.d.ts +53 -0
- package/src/types.ts +247 -0
- package/src/utils/canvasAddText.ts +72 -0
- package/src/utils/canvasUtils.ts +406 -0
- package/src/utils/deviceUtils.ts +31 -0
- package/src/utils/draftTemplateUtils.ts +70 -0
- package/src/utils/grid.ts +154 -0
- package/src/utils/imageUtils.ts +218 -0
- package/src/utils/job.ts +128 -0
- package/src/utils/keyboard.ts +28 -0
- package/src/utils/previewUtils.ts +43 -0
- package/src/utils/psdConverter.ts +595 -0
- package/src/utils/psdRenderUtils.ts +453 -0
- package/src/utils/renderUtils.ts +44 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { fabric } from 'fabric';
|
|
2
|
+
import { readPsd } from 'ag-psd';
|
|
3
|
+
import { generateUniqueId } from './job';
|
|
4
|
+
import { SavedCanvasObject } from '../types';
|
|
5
|
+
|
|
6
|
+
// Extended Canvas interface with our custom properties
|
|
7
|
+
interface ExtendedCanvas extends fabric.Canvas {
|
|
8
|
+
designBounds?: {
|
|
9
|
+
left: number;
|
|
10
|
+
top: number;
|
|
11
|
+
right: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
psdUrl?: string;
|
|
17
|
+
anythingABC?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts layers from a PSD file and renders them to a PNG
|
|
22
|
+
*
|
|
23
|
+
* @param psdUrl URL to the PSD file
|
|
24
|
+
* @returns Promise that resolves to a PNG data URL
|
|
25
|
+
*/
|
|
26
|
+
export const extractPsdBaseLayer = async (psdUrl: string): Promise<{ dataUrl: string, designBounds?: { left: number, top: number, right: number, bottom: number } }> => {
|
|
27
|
+
try {
|
|
28
|
+
|
|
29
|
+
// Fetch the PSD file
|
|
30
|
+
const response = await fetch(psdUrl, {
|
|
31
|
+
mode: 'cors',
|
|
32
|
+
credentials: 'same-origin',
|
|
33
|
+
headers: {
|
|
34
|
+
'Accept': '*/*',
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Failed to fetch PSD file: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
43
|
+
|
|
44
|
+
// Check if we actually got a PSD file (PSD files start with '8BPS')
|
|
45
|
+
const firstBytes = new Uint8Array(arrayBuffer, 0, 4);
|
|
46
|
+
const signature =
|
|
47
|
+
String.fromCharCode(firstBytes[0]) +
|
|
48
|
+
String.fromCharCode(firstBytes[1]) +
|
|
49
|
+
String.fromCharCode(firstBytes[2]) +
|
|
50
|
+
String.fromCharCode(firstBytes[3]);
|
|
51
|
+
|
|
52
|
+
if (signature !== '8BPS') {
|
|
53
|
+
// TODO handle different file types
|
|
54
|
+
console.warn('Warning: File does not have PSD signature. Got:', signature);
|
|
55
|
+
// Continue anyway - the server might be returning a different format
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Use ag-psd to read the PSD file
|
|
60
|
+
const psd = readPsd(
|
|
61
|
+
arrayBuffer,
|
|
62
|
+
{
|
|
63
|
+
skipCompositeImageData: false,
|
|
64
|
+
skipLayerImageData: false,
|
|
65
|
+
skipThumbnail: false
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Create a canvas to draw the layers
|
|
70
|
+
const canvas = document.createElement('canvas');
|
|
71
|
+
canvas.width = psd.width;
|
|
72
|
+
canvas.height = psd.height;
|
|
73
|
+
const ctx = canvas.getContext('2d');
|
|
74
|
+
|
|
75
|
+
if (!ctx) {
|
|
76
|
+
throw new Error('Could not get canvas context');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Variable to store Design layer bounds, if found
|
|
80
|
+
let designBounds: {
|
|
81
|
+
left: number,
|
|
82
|
+
top: number,
|
|
83
|
+
right: number,
|
|
84
|
+
bottom: number
|
|
85
|
+
} | undefined;
|
|
86
|
+
|
|
87
|
+
// If there are no layers or children, try to use the document itself
|
|
88
|
+
if (!psd.children || psd.children.length === 0) {
|
|
89
|
+
if (psd.imageData) {
|
|
90
|
+
// If we have imageData on the main document, use it
|
|
91
|
+
const imageData = new ImageData(
|
|
92
|
+
new Uint8ClampedArray(psd.imageData.data),
|
|
93
|
+
psd.width,
|
|
94
|
+
psd.height
|
|
95
|
+
);
|
|
96
|
+
ctx.putImageData(imageData, 0, 0);
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error('No layers or composite image data found in PSD');
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Categorize and sort layers to handle special layers appropriately
|
|
102
|
+
let layers = [...psd.children];
|
|
103
|
+
let designLayer = null;
|
|
104
|
+
let maskLayer = null;
|
|
105
|
+
let contentLayers: any[] = [];
|
|
106
|
+
|
|
107
|
+
// First pass - identify special layers and regular content layers
|
|
108
|
+
for (const layer of layers) {
|
|
109
|
+
if (layer.name === 'Design') {
|
|
110
|
+
designLayer = layer;
|
|
111
|
+
// Store design layer bounds for future reference
|
|
112
|
+
if (typeof layer.left === 'number' &&
|
|
113
|
+
typeof layer.top === 'number' &&
|
|
114
|
+
typeof layer.right === 'number' &&
|
|
115
|
+
typeof layer.bottom === 'number') {
|
|
116
|
+
designBounds = {
|
|
117
|
+
left: layer.left,
|
|
118
|
+
top: layer.top,
|
|
119
|
+
right: layer.right,
|
|
120
|
+
bottom: layer.bottom
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
} else if (layer.name === 'Mask') {
|
|
124
|
+
maskLayer = layer;
|
|
125
|
+
} else {
|
|
126
|
+
contentLayers.push(layer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Function to render a single layer
|
|
131
|
+
const renderLayer = (layer: any, ctx: CanvasRenderingContext2D) => {
|
|
132
|
+
// Check if the layer has a canvas
|
|
133
|
+
if (layer.canvas &&
|
|
134
|
+
typeof layer.left === 'number' &&
|
|
135
|
+
typeof layer.top === 'number') {
|
|
136
|
+
|
|
137
|
+
// Apply layer blending and opacity
|
|
138
|
+
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
|
139
|
+
|
|
140
|
+
// Draw this layer onto the main canvas at its position
|
|
141
|
+
ctx.drawImage(
|
|
142
|
+
layer.canvas,
|
|
143
|
+
layer.left,
|
|
144
|
+
layer.top
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Reset alpha
|
|
148
|
+
ctx.globalAlpha = 1;
|
|
149
|
+
} else if (layer.imageData &&
|
|
150
|
+
typeof layer.left === 'number' &&
|
|
151
|
+
typeof layer.top === 'number') {
|
|
152
|
+
|
|
153
|
+
// Create a temporary canvas for this layer
|
|
154
|
+
const layerCanvas = document.createElement('canvas');
|
|
155
|
+
const width = layer.right - layer.left;
|
|
156
|
+
const height = layer.bottom - layer.top;
|
|
157
|
+
layerCanvas.width = width;
|
|
158
|
+
layerCanvas.height = height;
|
|
159
|
+
const layerCtx = layerCanvas.getContext('2d');
|
|
160
|
+
|
|
161
|
+
if (layerCtx && layer.imageData) {
|
|
162
|
+
// Create an ImageData object from the PSD image data
|
|
163
|
+
const imageData = new ImageData(
|
|
164
|
+
new Uint8ClampedArray(layer.imageData.data),
|
|
165
|
+
width,
|
|
166
|
+
height
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Put the layer's image data on this canvas
|
|
170
|
+
layerCtx.putImageData(imageData, 0, 0);
|
|
171
|
+
|
|
172
|
+
// Apply layer blending and opacity
|
|
173
|
+
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
|
174
|
+
|
|
175
|
+
// Draw this layer onto the main canvas at its position
|
|
176
|
+
ctx.drawImage(
|
|
177
|
+
layerCanvas,
|
|
178
|
+
layer.left,
|
|
179
|
+
layer.top
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Reset alpha
|
|
183
|
+
ctx.globalAlpha = 1;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
console.warn(`Layer "${layer.name}" has no canvas or imageData`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Function to recursively process layers and their children
|
|
191
|
+
const processLayer = (layer: any, ctx: CanvasRenderingContext2D) => {
|
|
192
|
+
// Skip hidden layers
|
|
193
|
+
if (layer.hidden) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// If this is a group layer with children, process its children
|
|
198
|
+
if (layer.children && layer.children.length > 0) {
|
|
199
|
+
// Process children
|
|
200
|
+
const children = [...layer.children];
|
|
201
|
+
children.forEach(child => processLayer(child, ctx));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Render the actual layer
|
|
206
|
+
renderLayer(layer, ctx);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Render in the correct order:
|
|
210
|
+
// 1. First render the "Design" layer (margins/boundaries)
|
|
211
|
+
if (designLayer) {
|
|
212
|
+
processLayer(designLayer, ctx);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 2. Then render all content layers
|
|
216
|
+
for (const layer of contentLayers) {
|
|
217
|
+
processLayer(layer, ctx);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 3. Finally render the "Mask" layer on top to hide overflow
|
|
221
|
+
if (maskLayer) {
|
|
222
|
+
processLayer(maskLayer, ctx);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
227
|
+
|
|
228
|
+
// Return both the data URL and the design bounds if found
|
|
229
|
+
return { dataUrl, designBounds };
|
|
230
|
+
} catch (parseError) {
|
|
231
|
+
// Try an alternative fallback approach - direct image extraction
|
|
232
|
+
// Create a blob from the buffer and create an object URL
|
|
233
|
+
const blob = new Blob([arrayBuffer], {
|
|
234
|
+
type: response.headers.get('content-type') || 'application/octet-stream'
|
|
235
|
+
});
|
|
236
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
237
|
+
|
|
238
|
+
// Try loading as a regular image if the server actually returned an image
|
|
239
|
+
const img = new Image();
|
|
240
|
+
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
img.onload = () => {
|
|
243
|
+
const canvas = document.createElement('canvas');
|
|
244
|
+
canvas.width = img.width;
|
|
245
|
+
canvas.height = img.height;
|
|
246
|
+
const ctx = canvas.getContext('2d');
|
|
247
|
+
|
|
248
|
+
if (!ctx) {
|
|
249
|
+
reject(new Error('Could not get canvas context'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
ctx.drawImage(img, 0, 0);
|
|
254
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
255
|
+
URL.revokeObjectURL(objectUrl);
|
|
256
|
+
resolve({ dataUrl });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
img.onerror = () => {
|
|
260
|
+
URL.revokeObjectURL(objectUrl);
|
|
261
|
+
reject(new Error('Could not parse PSD or load as image'));
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
img.src = objectUrl;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Loads a PSD file onto a fabric.js canvas
|
|
274
|
+
*
|
|
275
|
+
* @param canvas fabric.js Canvas instance
|
|
276
|
+
* @param psdUrl URL to the PSD file
|
|
277
|
+
* @param width Canvas width
|
|
278
|
+
* @param height Canvas height
|
|
279
|
+
* @returns Promise that resolves when the image is added to the canvas
|
|
280
|
+
*/
|
|
281
|
+
export const loadPsdOntoCanvas = async (
|
|
282
|
+
canvas: ExtendedCanvas,
|
|
283
|
+
psdUrl: string,
|
|
284
|
+
variations: any[],
|
|
285
|
+
savedObjects: SavedCanvasObject[],
|
|
286
|
+
width: number,
|
|
287
|
+
height: number
|
|
288
|
+
): Promise<void> => {
|
|
289
|
+
try {
|
|
290
|
+
// Store the canvas reference in a variable for checking
|
|
291
|
+
const canvasRef = canvas;
|
|
292
|
+
|
|
293
|
+
// Store the current psdUrl on the canvas to help track which operation is active
|
|
294
|
+
canvasRef.psdUrl = psdUrl;
|
|
295
|
+
|
|
296
|
+
// Get the rendered PSD and design bounds if available
|
|
297
|
+
const { dataUrl, designBounds } = await extractPsdBaseLayer(psdUrl);
|
|
298
|
+
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
// Check if the canvas is still the same one that requested this operation
|
|
301
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
302
|
+
console.warn('Canvas changed or disposed during PSD processing, aborting.');
|
|
303
|
+
resolve();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fabric.Image.fromURL(dataUrl, (img) => {
|
|
308
|
+
// Check again if the canvas is still valid
|
|
309
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
310
|
+
console.warn('Canvas changed or disposed during image loading, aborting.');
|
|
311
|
+
resolve();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!img || !img.width || !img.height) {
|
|
316
|
+
reject(new Error('Failed to create fabric image from PSD'));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Scale image to fit canvas
|
|
321
|
+
const scale = Math.min(
|
|
322
|
+
width / img.width,
|
|
323
|
+
height / img.height
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Center the image - important for design bounds calculation
|
|
327
|
+
const imgLeft = (width - img.width * scale) / 2;
|
|
328
|
+
const imgTop = (height - img.height * scale) / 2;
|
|
329
|
+
|
|
330
|
+
// Set image scale and position
|
|
331
|
+
img.scaleX = scale;
|
|
332
|
+
img.scaleY = scale;
|
|
333
|
+
img.left = imgLeft;
|
|
334
|
+
img.top = imgTop;
|
|
335
|
+
img.selectable = false;
|
|
336
|
+
img.evented = false;
|
|
337
|
+
|
|
338
|
+
// Add image to canvas - check canvas validity first
|
|
339
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
340
|
+
console.warn('Canvas changed or disposed before adding image, aborting.');
|
|
341
|
+
resolve();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
canvasRef.add(img);
|
|
346
|
+
|
|
347
|
+
// If we have design bounds, store them for reference
|
|
348
|
+
if (designBounds) {
|
|
349
|
+
// Calculate scaled design bounds
|
|
350
|
+
const scaledDesignBounds = {
|
|
351
|
+
left: imgLeft + (designBounds.left * scale),
|
|
352
|
+
top: imgTop + (designBounds.top * scale),
|
|
353
|
+
width: (designBounds.right - designBounds.left) * scale,
|
|
354
|
+
height: (designBounds.bottom - designBounds.top) * scale,
|
|
355
|
+
right: imgLeft + (designBounds.right * scale),
|
|
356
|
+
bottom: imgTop + (designBounds.bottom * scale)
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Check canvas validity before continuing
|
|
360
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
361
|
+
console.warn('Canvas changed or disposed before adding design bounds, aborting.');
|
|
362
|
+
resolve();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Store on the canvas for future reference
|
|
367
|
+
canvasRef.designBounds = scaledDesignBounds;
|
|
368
|
+
|
|
369
|
+
// Add a visual indicator of the design area
|
|
370
|
+
const boundaryRect = new fabric.Rect(({
|
|
371
|
+
left: scaledDesignBounds.left,
|
|
372
|
+
top: scaledDesignBounds.top,
|
|
373
|
+
width: scaledDesignBounds.width,
|
|
374
|
+
height: scaledDesignBounds.height,
|
|
375
|
+
fill: 'transparent',
|
|
376
|
+
stroke: 'rgba(255, 0, 0, 0.5)',
|
|
377
|
+
strokeWidth: 1,
|
|
378
|
+
strokeDashArray: [5, 5],
|
|
379
|
+
selectable: false,
|
|
380
|
+
evented: false,
|
|
381
|
+
id: 'design-boundary-rect'
|
|
382
|
+
} as any));
|
|
383
|
+
|
|
384
|
+
// Check canvas validity before adding rect
|
|
385
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
386
|
+
console.warn('Canvas changed or disposed before adding boundary rect, aborting.');
|
|
387
|
+
resolve();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
canvasRef.add(boundaryRect);
|
|
392
|
+
|
|
393
|
+
// Create a clip path for the text based on design bounds
|
|
394
|
+
const clipPath = new fabric.Rect({
|
|
395
|
+
left: scaledDesignBounds.left,
|
|
396
|
+
top: scaledDesignBounds.top,
|
|
397
|
+
width: scaledDesignBounds.width,
|
|
398
|
+
height: scaledDesignBounds.height,
|
|
399
|
+
absolutePositioned: true
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Convert variations into canvas objects which obay the design bounds here
|
|
403
|
+
if (variations && variations.length > 0) {
|
|
404
|
+
// Process each variation to create appropriate canvas objects
|
|
405
|
+
variations.forEach(variation => {
|
|
406
|
+
if (!variation) {
|
|
407
|
+
return; // Skip invalid variations
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Get common positioning within design bounds
|
|
411
|
+
const objectLeft = scaledDesignBounds.left + scaledDesignBounds.width / 2;
|
|
412
|
+
const objectTop = scaledDesignBounds.top + scaledDesignBounds.height / 2;
|
|
413
|
+
|
|
414
|
+
// Extract the field type and value from the variation
|
|
415
|
+
const { canvasObjectType, file, fieldId, fontFamily, fontSize } = variation;
|
|
416
|
+
const value = variation.value;
|
|
417
|
+
|
|
418
|
+
// Check if this variation exists in savedObjects
|
|
419
|
+
const savedObject = savedObjects.find(obj => obj.fieldId === fieldId);
|
|
420
|
+
|
|
421
|
+
// Create canvas
|
|
422
|
+
let canvasObject;
|
|
423
|
+
|
|
424
|
+
// Handle text variations
|
|
425
|
+
if (canvasObjectType === 'text') {
|
|
426
|
+
// Create the text object with either saved or defaults
|
|
427
|
+
if (savedObject) {
|
|
428
|
+
// Create text with default first
|
|
429
|
+
canvasObject = new fabric.IText(value?.toString() || 'Text', {
|
|
430
|
+
...savedObject,
|
|
431
|
+
left: savedObject.left || objectLeft,
|
|
432
|
+
top: savedObject.top || objectTop,
|
|
433
|
+
fontFamily: savedObject.fontFamily || fontFamily,
|
|
434
|
+
fontSize: savedObject.fontSize || fontSize,
|
|
435
|
+
fill: savedObject.fill || '#000000',
|
|
436
|
+
originX: savedObject.originX || 'center',
|
|
437
|
+
originY: savedObject.originY ||'center',
|
|
438
|
+
selectable: true,
|
|
439
|
+
editable: true,
|
|
440
|
+
text: value?.toString(), // Always use the current variation value for text
|
|
441
|
+
scaleX: savedObject.scaleX || 1,
|
|
442
|
+
scaleY: savedObject.scaleY || 1,
|
|
443
|
+
angle: savedObject.angle || 0,
|
|
444
|
+
});
|
|
445
|
+
} else {
|
|
446
|
+
// Create with default properties
|
|
447
|
+
canvasObject = new fabric.IText(value?.toString() || 'Text', {
|
|
448
|
+
left: objectLeft,
|
|
449
|
+
top: objectTop,
|
|
450
|
+
fontFamily,
|
|
451
|
+
fontSize,
|
|
452
|
+
fill: '#000000',
|
|
453
|
+
originX: 'center',
|
|
454
|
+
originY: 'center',
|
|
455
|
+
selectable: true,
|
|
456
|
+
editable: false,
|
|
457
|
+
text: value?.toString(),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Store fieldId as a custom property
|
|
462
|
+
(canvasObject as any).fieldId = fieldId;
|
|
463
|
+
// Store uniqueId if it exists in the saved object
|
|
464
|
+
(canvasObject as any).uniqueId = savedObject?.uniqueId || generateUniqueId();
|
|
465
|
+
|
|
466
|
+
// Apply the clip path to the text
|
|
467
|
+
// Omit clip path for now to restore movability
|
|
468
|
+
canvasObject.clipPath = clipPath;
|
|
469
|
+
|
|
470
|
+
canvasRef.add(canvasObject);
|
|
471
|
+
}
|
|
472
|
+
// Handle image variations
|
|
473
|
+
else if (canvasObjectType === "image") {
|
|
474
|
+
// Get the file ID for comparison with saved objects
|
|
475
|
+
const fileId = file?.id;
|
|
476
|
+
// Find by both fieldId and fileId for images
|
|
477
|
+
const savedImageObject = fileId ?
|
|
478
|
+
savedObjects.find(obj => obj.fieldId === fieldId && obj.fileId === fileId) :
|
|
479
|
+
savedObject;
|
|
480
|
+
|
|
481
|
+
if (file && file.viewUrl) {
|
|
482
|
+
fabric.Image.fromURL(file.viewUrl, (img) => {
|
|
483
|
+
if (!img) return;
|
|
484
|
+
|
|
485
|
+
// Default scaling to fit design bounds
|
|
486
|
+
const maxWidth = scaledDesignBounds.width * 0.8;
|
|
487
|
+
const maxHeight = scaledDesignBounds.height * 0.8;
|
|
488
|
+
const defaultScale = Math.min(
|
|
489
|
+
maxWidth / img.width!,
|
|
490
|
+
maxHeight / img.height!,
|
|
491
|
+
1
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (savedImageObject) {
|
|
495
|
+
// Apply saved properties
|
|
496
|
+
img.set({
|
|
497
|
+
...savedImageObject,
|
|
498
|
+
left: savedImageObject.left || objectLeft,
|
|
499
|
+
top: savedImageObject.top || objectTop,
|
|
500
|
+
scaleX: savedImageObject.scaleX || defaultScale,
|
|
501
|
+
scaleY: savedImageObject.scaleY || defaultScale,
|
|
502
|
+
angle: savedImageObject.angle || 0,
|
|
503
|
+
originX: savedImageObject.originX || 'center',
|
|
504
|
+
originY: savedImageObject.originY || 'center',
|
|
505
|
+
cornerSize: 8,
|
|
506
|
+
borderColor: '#303DBF',
|
|
507
|
+
cornerColor: '#303DBF',
|
|
508
|
+
cornerStrokeColor: '#303DBF',
|
|
509
|
+
transparentCorners: false,
|
|
510
|
+
selectable: true,
|
|
511
|
+
crossOrigin: 'anonymous',
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Store uniqueId if it exists
|
|
515
|
+
(img as any).uniqueId = savedImageObject?.uniqueId || generateUniqueId();
|
|
516
|
+
|
|
517
|
+
} else {
|
|
518
|
+
// Apply default properties
|
|
519
|
+
img.scale(defaultScale);
|
|
520
|
+
img.set({
|
|
521
|
+
left: objectLeft,
|
|
522
|
+
top: objectTop,
|
|
523
|
+
originX: 'center',
|
|
524
|
+
originY: 'center',
|
|
525
|
+
cornerSize: 8,
|
|
526
|
+
borderColor: '#303DBF',
|
|
527
|
+
cornerColor: '#303DBF',
|
|
528
|
+
cornerStrokeColor: '#303DBF',
|
|
529
|
+
transparentCorners: false,
|
|
530
|
+
selectable: true,
|
|
531
|
+
crossOrigin: 'anonymous',
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Store fieldId and fileId as custom properties
|
|
536
|
+
(img as any).fieldId = fieldId;
|
|
537
|
+
if (fileId) {
|
|
538
|
+
(img as any).fileId = fileId;
|
|
539
|
+
}
|
|
540
|
+
(img as any).uniqueId = generateUniqueId();
|
|
541
|
+
|
|
542
|
+
// Apply clip path
|
|
543
|
+
const imageClipPath = new fabric.Rect({
|
|
544
|
+
left: scaledDesignBounds.left,
|
|
545
|
+
top: scaledDesignBounds.top,
|
|
546
|
+
width: scaledDesignBounds.width,
|
|
547
|
+
height: scaledDesignBounds.height,
|
|
548
|
+
absolutePositioned: true
|
|
549
|
+
});
|
|
550
|
+
img.clipPath = imageClipPath;
|
|
551
|
+
|
|
552
|
+
// Check canvas validity before adding
|
|
553
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
canvasRef.add(img);
|
|
557
|
+
canvasRef.renderAll();
|
|
558
|
+
}, { crossOrigin: 'anonymous' });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check canvas validity before adding text
|
|
565
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
566
|
+
console.warn('Canvas changed or disposed before adding test text, aborting.');
|
|
567
|
+
resolve();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Final check before renderAll
|
|
573
|
+
if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
|
|
574
|
+
console.warn('Canvas changed or disposed before final render, aborting.');
|
|
575
|
+
resolve();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Try to render the canvas, with error handling
|
|
580
|
+
try {
|
|
581
|
+
if (canvasRef && canvasRef.renderAll) {
|
|
582
|
+
canvasRef.renderAll();
|
|
583
|
+
}
|
|
584
|
+
} catch (e) {
|
|
585
|
+
console.error('Error rendering canvas:', e);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
resolve();
|
|
589
|
+
}, { crossOrigin: 'anonymous' });
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error('Error loading PSD onto canvas:', error);
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
};
|