@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.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/README.md +174 -0
  3. package/dist/components/AlignMenu.d.ts +3 -0
  4. package/dist/components/AlignMenu.js +61 -0
  5. package/dist/components/FloatingToolbar.d.ts +4 -0
  6. package/dist/components/FloatingToolbar.js +97 -0
  7. package/dist/components/FormatButtons.d.ts +2 -0
  8. package/dist/components/FormatButtons.js +22 -0
  9. package/dist/components/ImageNavButton.d.ts +7 -0
  10. package/dist/components/ImageNavButton.js +14 -0
  11. package/dist/components/ImageZoomModal.d.ts +12 -0
  12. package/dist/components/ImageZoomModal.js +129 -0
  13. package/dist/components/LayerPanel.d.ts +4 -0
  14. package/dist/components/LayerPanel.js +223 -0
  15. package/dist/components/LoadingOverlay.d.ts +9 -0
  16. package/dist/components/LoadingOverlay.js +17 -0
  17. package/dist/components/ProductEditor.d.ts +4 -0
  18. package/dist/components/ProductEditor.js +67 -0
  19. package/dist/components/ProductImageGallery.d.ts +6 -0
  20. package/dist/components/ProductImageGallery.js +148 -0
  21. package/dist/components/ProductPreviews.d.ts +7 -0
  22. package/dist/components/ProductPreviews.js +281 -0
  23. package/dist/components/TextToolbar.d.ts +4 -0
  24. package/dist/components/TextToolbar.js +117 -0
  25. package/dist/components/Toolbar.d.ts +3 -0
  26. package/dist/components/Toolbar.js +47 -0
  27. package/dist/config/colorConfig.d.ts +1 -0
  28. package/dist/config/colorConfig.js +10 -0
  29. package/dist/config/fontConfig.d.ts +5 -0
  30. package/dist/config/fontConfig.js +21 -0
  31. package/dist/context/ProductEditorContext.d.ts +60 -0
  32. package/dist/context/ProductEditorContext.js +743 -0
  33. package/dist/hooks/useSwipeNavigate.d.ts +11 -0
  34. package/dist/hooks/useSwipeNavigate.js +35 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +30 -0
  37. package/dist/styles/BottomPreviewDisplay.css +65 -0
  38. package/dist/styles/FloatingToolbar.css +41 -0
  39. package/dist/styles/ImageGallery.css +147 -0
  40. package/dist/styles/ImageNavButton.css +35 -0
  41. package/dist/styles/ImageZoomModal.css +101 -0
  42. package/dist/styles/LayerPanel.css +179 -0
  43. package/dist/styles/LoadingOverlay.css +48 -0
  44. package/dist/styles/ProductEditor.css +192 -0
  45. package/dist/styles/TextToolbar.css +327 -0
  46. package/dist/styles/Toolbar.css +125 -0
  47. package/dist/styles/index.css +5 -0
  48. package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
  49. package/dist/styles/textToolbarSelectStyles.js +30 -0
  50. package/dist/types.d.ts +228 -0
  51. package/dist/types.js +17 -0
  52. package/dist/utils/canvasAddText.d.ts +11 -0
  53. package/dist/utils/canvasAddText.js +66 -0
  54. package/dist/utils/canvasUtils.d.ts +37 -0
  55. package/dist/utils/canvasUtils.js +342 -0
  56. package/dist/utils/deviceUtils.d.ts +5 -0
  57. package/dist/utils/deviceUtils.js +33 -0
  58. package/dist/utils/draftTemplateUtils.d.ts +23 -0
  59. package/dist/utils/draftTemplateUtils.js +66 -0
  60. package/dist/utils/grid.d.ts +32 -0
  61. package/dist/utils/grid.js +151 -0
  62. package/dist/utils/imageUtils.d.ts +12 -0
  63. package/dist/utils/imageUtils.js +178 -0
  64. package/dist/utils/job.d.ts +40 -0
  65. package/dist/utils/job.js +115 -0
  66. package/dist/utils/keyboard.d.ts +1 -0
  67. package/dist/utils/keyboard.js +26 -0
  68. package/dist/utils/previewUtils.d.ts +9 -0
  69. package/dist/utils/previewUtils.js +39 -0
  70. package/dist/utils/psdConverter.d.ts +40 -0
  71. package/dist/utils/psdConverter.js +516 -0
  72. package/dist/utils/psdRenderUtils.d.ts +12 -0
  73. package/dist/utils/psdRenderUtils.js +465 -0
  74. package/dist/utils/renderUtils.d.ts +2 -0
  75. package/dist/utils/renderUtils.js +85 -0
  76. package/package.json +66 -0
  77. package/src/components/AlignMenu.tsx +49 -0
  78. package/src/components/FloatingToolbar.tsx +83 -0
  79. package/src/components/FormatButtons.tsx +31 -0
  80. package/src/components/ImageNavButton.tsx +23 -0
  81. package/src/components/ImageZoomModal.tsx +163 -0
  82. package/src/components/LayerPanel.tsx +256 -0
  83. package/src/components/LoadingOverlay.tsx +27 -0
  84. package/src/components/ProductEditor.tsx +104 -0
  85. package/src/components/ProductImageGallery.tsx +181 -0
  86. package/src/components/ProductPreviews.tsx +243 -0
  87. package/src/components/TextToolbar.tsx +164 -0
  88. package/src/components/Toolbar.tsx +86 -0
  89. package/src/config/colorConfig.ts +7 -0
  90. package/src/config/fontConfig.ts +23 -0
  91. package/src/context/ProductEditorContext.tsx +829 -0
  92. package/src/hooks/useSwipeNavigate.ts +40 -0
  93. package/src/index.ts +6 -0
  94. package/src/styles/BottomPreviewDisplay.css +65 -0
  95. package/src/styles/FloatingToolbar.css +41 -0
  96. package/src/styles/ImageGallery.css +147 -0
  97. package/src/styles/ImageNavButton.css +35 -0
  98. package/src/styles/ImageZoomModal.css +101 -0
  99. package/src/styles/LayerPanel.css +179 -0
  100. package/src/styles/LoadingOverlay.css +48 -0
  101. package/src/styles/ProductEditor.css +192 -0
  102. package/src/styles/TextToolbar.css +327 -0
  103. package/src/styles/Toolbar.css +125 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/textToolbarSelectStyles.ts +60 -0
  106. package/src/types/ag-psd/index.d.ts +1 -0
  107. package/src/types/fabric.d.ts +14 -0
  108. package/src/types/lodash/index.d.ts +1 -0
  109. package/src/types/psd.d.ts +53 -0
  110. package/src/types.ts +247 -0
  111. package/src/utils/canvasAddText.ts +72 -0
  112. package/src/utils/canvasUtils.ts +406 -0
  113. package/src/utils/deviceUtils.ts +31 -0
  114. package/src/utils/draftTemplateUtils.ts +70 -0
  115. package/src/utils/grid.ts +154 -0
  116. package/src/utils/imageUtils.ts +218 -0
  117. package/src/utils/job.ts +128 -0
  118. package/src/utils/keyboard.ts +28 -0
  119. package/src/utils/previewUtils.ts +43 -0
  120. package/src/utils/psdConverter.ts +595 -0
  121. package/src/utils/psdRenderUtils.ts +453 -0
  122. package/src/utils/renderUtils.ts +44 -0
  123. package/tsconfig.json +17 -0
@@ -0,0 +1,406 @@
1
+ import { fabric } from 'fabric';
2
+
3
+ /**
4
+ * Adds an editable text object to the canvas
5
+ * @param canvas - The Fabric.js canvas instance
6
+ * @param width - The width of the canvas
7
+ * @param height - The height of the canvas
8
+ * @param defaultText - Optional default text (defaults to "Text")
9
+ * @param fontSize - Optional font size (defaults to 24)
10
+ * @param fontFamily - Optional font family (defaults to Arial)
11
+ */
12
+ export const addTextToCanvas = (
13
+ canvas: fabric.Canvas,
14
+ width: number,
15
+ height: number,
16
+ defaultText: string = 'Text',
17
+ fontSize: number = 24,
18
+ fontFamily: string = 'Nunito'
19
+ ) => {
20
+ if (!canvas) return null;
21
+
22
+ try {
23
+ // Safe check if canvas is still valid
24
+ if (!canvas.getElement() || !canvas.getElement().parentNode) {
25
+ return null;
26
+ }
27
+
28
+ // Create a new text object with minimal options
29
+ const text = new fabric.IText(
30
+ defaultText,
31
+ {
32
+ left: width / 2,
33
+ top: height / 2,
34
+ fontFamily: fontFamily,
35
+ fontSize: fontSize,
36
+ fill: '#000000',
37
+ originX: 'center',
38
+ originY: 'center',
39
+ selectable: true,
40
+ editable: false
41
+ }
42
+ );
43
+
44
+ // Add the text to the canvas
45
+ canvas.add(text);
46
+
47
+ // Ensure the text is rendered
48
+ canvas.renderAll();
49
+
50
+ // Make the text active - this is critical
51
+ const setActiveTextObject = () => {
52
+ try {
53
+ // Check if canvas is still valid
54
+ if (canvas && canvas.getElement() && canvas.getElement().parentNode) {
55
+ canvas.setActiveObject(text);
56
+ canvas.renderAll();
57
+ }
58
+ } catch (err) {
59
+ console.error('Error setting active text object:', err);
60
+ }
61
+ };
62
+
63
+ // Initial selection
64
+ setActiveTextObject();
65
+
66
+ // Delayed selection to ensure text becomes active
67
+ setTimeout(setActiveTextObject, 50);
68
+
69
+ return text;
70
+ } catch (error) {
71
+ console.error('Error adding text to canvas:', error);
72
+ return null;
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Renders the entire canvas as an image without displaying grid elements
78
+ *
79
+ * @param canvas - The Fabric.js canvas instance
80
+ * @param options - Optional rendering options
81
+ * @returns A promise that resolves to the dataURL of the canvas without grid
82
+ */
83
+ export const renderCanvasWithoutGrid = (
84
+ canvas: fabric.Canvas,
85
+ options?: {
86
+ format?: string;
87
+ quality?: number;
88
+ multiplier?: number;
89
+ backgroundColor?: string | null;
90
+ }
91
+ ): Promise<string | null> => {
92
+ return new Promise((resolve) => {
93
+ if (!canvas) {
94
+ console.error('Canvas is null or undefined');
95
+ resolve(null);
96
+ return;
97
+ }
98
+
99
+ try {
100
+ // Get all objects on the canvas
101
+ const objects = canvas.getObjects();
102
+
103
+ // Store original visibility of each object
104
+ const originalVisibilities = objects.map(obj => ({
105
+ object: obj,
106
+ visible: obj.visible
107
+ }));
108
+
109
+ // Hide grid elements - typically grid elements might have a specific ID or class
110
+ // You may need to adjust this logic based on how grid elements are identified
111
+ objects.forEach(obj => {
112
+ // Hide objects that represent grid elements
113
+ // This assumes grid elements have a property like 'type' or 'id' that identifies them
114
+ if (
115
+ (obj as any).id?.includes('grid') ||
116
+ (obj as any).type === 'grid' ||
117
+ (obj as any).isGridElement ||
118
+ // Add more comprehensive grid detection
119
+ (obj as any).data?.isGrid ||
120
+ (obj as any).name?.includes('grid') ||
121
+ (obj as any).className?.includes('grid') ||
122
+ // Check for common grid-related properties
123
+ (obj as any).strokeDashArray || // Dashed lines are often used for grids
124
+ // Check for line objects with evenly spaced positions (typical grid pattern)
125
+ ((obj as any).type === 'line' && ((obj as any).x1 === (obj as any).x2 || (obj as any).y1 === (obj as any).y2)) ||
126
+ // Check for objects tagged with grid-related classes
127
+ (obj as any).classes?.includes('grid') ||
128
+ // Check for objects with certain grid styling
129
+ ((obj as any).stroke &&
130
+ ((obj as any).strokeWidth === 0.5 || (obj as any).strokeWidth === 1) &&
131
+ ((obj as any).stroke === '#ddd' || (obj as any).stroke === '#eee' || (obj as any).stroke === 'rgba(0,0,0,0.1)'))
132
+ ) {
133
+ obj.visible = false;
134
+ }
135
+ });
136
+
137
+ // Save the original selection state
138
+ const originalSelectionBorder = canvas.selectionBorderColor;
139
+ const originalSelectionColor = canvas.selectionColor;
140
+ const originalActiveObject = canvas.getActiveObject();
141
+
142
+ // Temporarily disable selection indicators
143
+ canvas.selectionBorderColor = 'transparent';
144
+ canvas.selectionColor = 'transparent';
145
+ canvas.discardActiveObject();
146
+
147
+ // Apply any background color if specified
148
+ const originalBackgroundColor = canvas.backgroundColor;
149
+ if (options?.backgroundColor !== undefined) {
150
+ canvas.backgroundColor = options.backgroundColor || 'transparent';
151
+ }
152
+
153
+ // Render the canvas
154
+ canvas.renderAll();
155
+
156
+ // Get rendering options
157
+ const format = options?.format || 'png';
158
+ const quality = options?.quality || 1;
159
+ const multiplier = options?.multiplier || 1;
160
+
161
+ // Create a data URL from the canvas
162
+ try {
163
+ const dataUrl = canvas.toDataURL({
164
+ format: format,
165
+ quality: quality,
166
+ multiplier: multiplier
167
+ });
168
+
169
+ // Restore the original state
170
+ objects.forEach((obj, index) => {
171
+ obj.visible = originalVisibilities[index].visible;
172
+ });
173
+
174
+ // Restore selection state
175
+ canvas.selectionBorderColor = originalSelectionBorder;
176
+ canvas.selectionColor = originalSelectionColor;
177
+ if (originalActiveObject) {
178
+ canvas.setActiveObject(originalActiveObject);
179
+ }
180
+
181
+ // Restore background color
182
+ canvas.backgroundColor = originalBackgroundColor;
183
+
184
+ // Render with original state
185
+ canvas.renderAll();
186
+
187
+ resolve(dataUrl);
188
+ } catch (error) {
189
+ console.error('Error generating data URL:', error);
190
+ resolve(null);
191
+ }
192
+ } catch (error) {
193
+ console.error('Error rendering canvas without grid:', error);
194
+ resolve(null);
195
+ }
196
+ });
197
+ };
198
+
199
+ /**
200
+ * Renders an image containing only what's visible inside a clipPath on the canvas
201
+ *
202
+ * @param canvas - The Fabric.js canvas instance
203
+ * @param options - Optional rendering options
204
+ * @returns A promise that resolves to the dataURL of the contents inside the clipPath, or null if no clipped object is found
205
+ */
206
+ export const renderClippedImage = (
207
+ canvas: fabric.Canvas,
208
+ options?: {
209
+ format?: string;
210
+ quality?: number;
211
+ multiplier?: number;
212
+ backgroundColor?: string;
213
+ }
214
+ ): Promise<string | null> => {
215
+ return new Promise((resolve) => {
216
+ if (!canvas) {
217
+ console.error('Canvas is null or undefined');
218
+ resolve(null);
219
+ return;
220
+ }
221
+
222
+ try {
223
+ // Find objects with clipPath
224
+ const objects = canvas.getObjects();
225
+ let targetObject: fabric.Object | null = null;
226
+
227
+ targetObject = objects.find((obj: any) => obj.id === 'design-boundary-rect') || null;
228
+
229
+ if (!targetObject) {
230
+ console.warn('No boundary object found');
231
+ resolve(null);
232
+ return;
233
+ }
234
+
235
+ // Get bounds of the object with clipPath
236
+ const objBounds = targetObject.getBoundingRect();
237
+
238
+ // Create a new canvas element
239
+ const dpr = window.devicePixelRatio || 1;
240
+
241
+ const tempCanvasEl = document.createElement('canvas');
242
+ tempCanvasEl.width = objBounds.width * dpr;
243
+ tempCanvasEl.height = objBounds.height * dpr;
244
+
245
+ tempCanvasEl.style.width = `${objBounds.width}px`;
246
+ tempCanvasEl.style.height = `${objBounds.height}px`;
247
+
248
+
249
+ // Explicitly set alpha to true to support transparency
250
+ const tempCtx = tempCanvasEl.getContext('2d', { alpha: true });
251
+ tempCtx && tempCtx.scale(dpr, dpr);
252
+
253
+ if (!tempCtx) {
254
+ console.error('Could not get canvas context');
255
+ resolve(null);
256
+ return;
257
+ }
258
+
259
+ // Clear the canvas to ensure transparency
260
+ tempCtx.clearRect(0, 0, tempCanvasEl.width, tempCanvasEl.height);
261
+
262
+ // Set background color if provided, otherwise keep transparent
263
+ if (options?.backgroundColor) {
264
+ tempCtx.fillStyle = options.backgroundColor;
265
+ tempCtx.fillRect(0, 0, tempCanvasEl.width, tempCanvasEl.height);
266
+ }
267
+
268
+ // Save current canvas state
269
+ const originalZoom = canvas.getZoom();
270
+ const originalViewportTransform = canvas.viewportTransform ? [...canvas.viewportTransform] : [1, 0, 0, 1, 0, 0];
271
+
272
+ // Save the original background color of the canvas
273
+ const originalBackgroundColor = canvas.backgroundColor;
274
+
275
+ // Temporarily set canvas background to transparent
276
+ canvas.backgroundColor = 'transparent';
277
+
278
+ // Store original object positions
279
+ const originalPositions = objects.map(obj => ({
280
+ object: obj,
281
+ left: obj.left,
282
+ top: obj.top,
283
+ scaleX: obj.scaleX,
284
+ scaleY: obj.scaleY
285
+ }));
286
+
287
+ // Temporarily modify the canvas view to focus on our object
288
+ canvas.setZoom(1);
289
+ canvas.setViewportTransform([1, 0, 0, 1, -objBounds.left, -objBounds.top]);
290
+
291
+ // Temporarily hide all objects except our target and its clipped content
292
+ const originalVisibilities = objects.map(obj => ({ object: obj, visible: obj.visible }));
293
+ objects.forEach(obj => {
294
+ if (!(obj as any).fieldId) {
295
+ obj.visible = false;
296
+ }
297
+ });
298
+
299
+ // Temporarily disable selection borders at canvas level
300
+ const originalSelectionBorder = canvas.selectionBorderColor;
301
+ const originalSelectionColor = canvas.selectionColor;
302
+ canvas.selectionBorderColor = 'transparent';
303
+ canvas.selectionColor = 'transparent';
304
+
305
+ // Temporarily disable selection borders at object level
306
+ const originalObjectStates = objects.map(obj => ({
307
+ object: obj,
308
+ borderColor: obj.borderColor,
309
+ cornerColor: obj.cornerColor,
310
+ cornerStrokeColor: obj.cornerStrokeColor,
311
+ transparentCorners: obj.transparentCorners,
312
+ hasControls: obj.hasControls,
313
+ hasBorders: obj.hasBorders
314
+ }));
315
+
316
+ objects.forEach(obj => {
317
+ obj.borderColor = 'transparent';
318
+ obj.cornerColor = 'transparent';
319
+ obj.cornerStrokeColor = 'transparent';
320
+ obj.transparentCorners = true;
321
+ obj.hasControls = false;
322
+ obj.hasBorders = false;
323
+ });
324
+
325
+ // Render just the isolated object
326
+ canvas.renderAll();
327
+
328
+ // Draw the current canvas view to our temp canvas
329
+ tempCtx.drawImage(
330
+ canvas.getElement(),
331
+ 0, 0, objBounds.width * dpr, objBounds.height * dpr,
332
+ 0, 0, objBounds.width, objBounds.height
333
+ );
334
+
335
+ // Restore the original object positions
336
+ originalPositions.forEach(item => {
337
+ item.object.left = item.left;
338
+ item.object.top = item.top;
339
+ item.object.scaleX = item.scaleX;
340
+ item.object.scaleY = item.scaleY;
341
+ item.object.setCoords();
342
+ });
343
+
344
+ // Restore the original canvas state
345
+ canvas.setZoom(originalZoom);
346
+ canvas.setViewportTransform(originalViewportTransform);
347
+ canvas.backgroundColor = originalBackgroundColor;
348
+ originalVisibilities.forEach(item => {
349
+ item.object.visible = item.visible;
350
+ });
351
+
352
+ // Restore object-level selection borders
353
+ originalObjectStates.forEach(state => {
354
+ state.object.borderColor = state.borderColor;
355
+ state.object.cornerColor = state.cornerColor;
356
+ state.object.cornerStrokeColor = state.cornerStrokeColor;
357
+ state.object.transparentCorners = state.transparentCorners;
358
+ state.object.hasControls = state.hasControls;
359
+ state.object.hasBorders = state.hasBorders;
360
+ });
361
+
362
+ // Restore canvas-level selection borders
363
+ canvas.selectionBorderColor = originalSelectionBorder;
364
+ canvas.selectionColor = originalSelectionColor;
365
+ canvas.renderAll();
366
+
367
+ // Get the data URL from the temp canvas
368
+ const format = options?.format || 'png';
369
+ const quality = options?.quality || 1;
370
+
371
+ // Create a new canvas for final rendering with proper transparency
372
+ const finalCanvas = document.createElement('canvas');
373
+ finalCanvas.width = tempCanvasEl.width;
374
+ finalCanvas.height = tempCanvasEl.height;
375
+ const finalCtx = finalCanvas.getContext('2d', { alpha: true });
376
+
377
+ finalCtx && finalCtx.scale(1, 1);
378
+
379
+ if (!finalCtx) {
380
+ console.error('Could not get final canvas context');
381
+ resolve(null);
382
+ return;
383
+ }
384
+
385
+ // Clear the final canvas to ensure transparency
386
+ finalCtx.clearRect(0, 0, finalCanvas.width, finalCanvas.height);
387
+
388
+ // Draw the temp canvas content to the final canvas
389
+ finalCtx.drawImage(tempCanvasEl, 0, 0);
390
+
391
+ // Try to get the data URL from the final canvas
392
+ try {
393
+ // Ensure PNG format for transparency support
394
+ const dataUrl = finalCanvas.toDataURL(`image/${format === 'jpg' ? 'png' : format}`, quality);
395
+ resolve(dataUrl);
396
+ } catch (error) {
397
+ console.error('Error getting data URL:', error);
398
+ resolve(null);
399
+ }
400
+
401
+ } catch (error) {
402
+ console.error('Error rendering clipped image:', error);
403
+ resolve(null);
404
+ }
405
+ });
406
+ };
@@ -0,0 +1,31 @@
1
+ export const getDeviceAdjustedDimensions = (defaultWidth = 800, defaultHeight = 600) => {
2
+ if (typeof window === 'undefined') {
3
+ return {
4
+ isMobile: false,
5
+ width: defaultWidth,
6
+ height: defaultHeight
7
+ };
8
+ }
9
+
10
+ const isMobile = window.innerWidth < 480;
11
+
12
+ // Use default dimensions for desktop
13
+ if (!isMobile) {
14
+ return {
15
+ isMobile: false,
16
+ width: defaultWidth,
17
+ height: defaultHeight
18
+ };
19
+ }
20
+
21
+ // Calculate mobile dimensions
22
+ const mobileWidth = Math.min(window.innerWidth * 0.9 - 32, 400);
23
+ const aspectRatio = defaultWidth / defaultHeight;
24
+ const mobileHeight = mobileWidth / aspectRatio;
25
+
26
+ return {
27
+ isMobile: true,
28
+ width: mobileWidth,
29
+ height: mobileHeight
30
+ };
31
+ };
@@ -0,0 +1,70 @@
1
+ import { DraftPreview, DraftTemplateData, RenderedDraftPreview, DraftPreviewLayer } from '../types';
2
+
3
+ /**
4
+ * Compares two arrays of draft templates to determine if they've changed significantly
5
+ * This helps prevent unnecessary re-renders and infinite loops
6
+ *
7
+ * @param oldTemplates The previous draft templates array
8
+ * @param newTemplates The new draft templates array
9
+ * @returns True if templates have changed, false if they're effectively the same
10
+ */
11
+ export function haveDraftTemplatesChanged(
12
+ oldTemplates: Array<{ template: any; variationObjects: any[] }>,
13
+ newTemplates: Array<{ template: any; variationObjects: any[] }>
14
+ ): boolean {
15
+ // Quick length check
16
+ if (!oldTemplates || !newTemplates) return true;
17
+ if (oldTemplates.length !== newTemplates.length) return true;
18
+
19
+ // Check if template IDs are the same and in the same order
20
+ const oldIds = oldTemplates.map(dt => dt.template.id).join(',');
21
+ const newIds = newTemplates.map(dt => dt.template.id).join(',');
22
+
23
+ if (oldIds !== newIds) return true;
24
+
25
+ // Deep check each template for significant changes
26
+ for (let i = 0; i < oldTemplates.length; i++) {
27
+ const oldTemplate = oldTemplates[i];
28
+ const newTemplate = newTemplates[i];
29
+
30
+ // Check for template property changes
31
+ if (oldTemplate.template.id !== newTemplate.template.id) return true;
32
+ if (oldTemplate.template.name !== newTemplate.template.name) return true;
33
+ if (oldTemplate.template.width !== newTemplate.template.width) return true;
34
+ if (oldTemplate.template.height !== newTemplate.template.height) return true;
35
+
36
+ // Check variation objects count
37
+ if (oldTemplate.variationObjects.length !== newTemplate.variationObjects.length) return true;
38
+
39
+ // We could do more detailed checking of variation objects here,
40
+ // but for performance we'll assume if counts are the same and template properties
41
+ // are the same, the variations probably haven't changed enough to warrant a full re-render
42
+ }
43
+
44
+ // Templates are effectively the same
45
+ return false;
46
+ }
47
+
48
+ export function mapPreviewsWithRendered(
49
+ draftTemplates: DraftTemplateData[],
50
+ draftPreviews: DraftPreview[],
51
+ renderedDraftPreviews: RenderedDraftPreview[]
52
+ ): { draftPreview: DraftPreview; draftPreviewLayers: { layerName: string | undefined; renderedLayer: RenderedDraftPreview | null }[] }[] {
53
+ return draftPreviews.map((draftPreview: DraftPreview) => {
54
+ // Map each draftTemplate to its associated layers and rendered layers
55
+ const draftPreviewLayers = draftTemplates.flatMap((dTD: DraftTemplateData) => {
56
+ const layers = dTD?.template?.draftPreviewLayers || [];
57
+ return layers
58
+ .filter((dPL: DraftPreviewLayer) => dPL?.draftPreview?.id === draftPreview.id)
59
+ .map(layer => {
60
+ const renderedLayer = renderedDraftPreviews.find(rDR => rDR?.templateId === dTD?.template?.id);
61
+ return { layerName: layer?.layerName, renderedLayer: renderedLayer || null };
62
+ });
63
+ });
64
+
65
+ return {
66
+ draftPreview,
67
+ draftPreviewLayers,
68
+ };
69
+ });
70
+ }
@@ -0,0 +1,154 @@
1
+ import { fabric } from 'fabric';
2
+
3
+ /**
4
+ * @param fabricCanvas
5
+ * @param width
6
+ * @param height
7
+ * @param gridSize
8
+ * @param lineColor
9
+ * @param enabled
10
+ */
11
+ export const drawGrid = (
12
+ fabricCanvas: fabric.Canvas,
13
+ width: number,
14
+ height: number,
15
+ gridSize = 20,
16
+ lineColor = '#e0e0e0',
17
+ enabled = true
18
+ ) => {
19
+ // First, make sure fabricCanvas is not null
20
+ if (!fabricCanvas) {
21
+ console.warn('drawGrid: fabricCanvas is null or undefined');
22
+ return;
23
+ }
24
+
25
+ // Make sure the canvas element exists and is valid
26
+ try {
27
+ // This will throw an error if the canvas element is no longer in the DOM
28
+ if (!fabricCanvas.getElement() || !fabricCanvas.getElement().parentNode) {
29
+ console.warn('drawGrid: Canvas element is not in the DOM');
30
+ return;
31
+ }
32
+ } catch (e) {
33
+ console.warn('drawGrid: Error checking canvas element:', e);
34
+ return;
35
+ }
36
+
37
+ // remove all existing grid lines
38
+ const gridLines = fabricCanvas.getObjects().filter(obj => obj.data?.isGrid);
39
+ gridLines.forEach(line => fabricCanvas.remove(line));
40
+
41
+ if (!enabled) return;
42
+
43
+ // draw vertical lines
44
+ for (let i = 0; i <= width; i += gridSize) {
45
+ const line = new fabric.Line([i, 0, i, height], {
46
+ stroke: lineColor,
47
+ strokeWidth: 0.5,
48
+ opacity: 0.3,
49
+ selectable: false,
50
+ evented: false,
51
+ data: { isGrid: true }
52
+ });
53
+ fabricCanvas.add(line);
54
+ }
55
+
56
+ // draw horizontal lines
57
+ for (let i = 0; i <= height; i += gridSize) {
58
+ const line = new fabric.Line([0, i, width, i], {
59
+ stroke: lineColor,
60
+ strokeWidth: 0.5,
61
+ opacity: 0.3,
62
+ selectable: false,
63
+ evented: false,
64
+ data: { isGrid: true }
65
+ });
66
+ fabricCanvas.add(line);
67
+ }
68
+
69
+ try {
70
+ fabricCanvas.renderAll();
71
+ } catch (e) {
72
+ console.error('drawGrid: Error rendering grid:', e);
73
+ }
74
+ };
75
+
76
+ /**
77
+ * save the grid state on the canvas
78
+ * @param fabricCanvas
79
+ */
80
+ export const saveGridState = (fabricCanvas: fabric.Canvas) => {
81
+ if (!fabricCanvas) {
82
+ console.warn('saveGridState: fabricCanvas is null or undefined');
83
+ return [];
84
+ }
85
+
86
+ try {
87
+ const objs = fabricCanvas.getObjects().filter(obj => obj);
88
+ return objs;
89
+ } catch (e) {
90
+ console.warn('saveGridState: Error getting grid objects:', e);
91
+ return [];
92
+ }
93
+ };
94
+
95
+ /**
96
+ * bring grid lines to front of the canvas
97
+ * @param fabricCanvas
98
+ * @param gridLines
99
+ */
100
+ export const bringGridToFront = (fabricCanvas: fabric.Canvas, gridLines: fabric.Object[]) => {
101
+ if (!fabricCanvas) {
102
+ console.warn('bringGridToFront: fabricCanvas is null or undefined');
103
+ return;
104
+ }
105
+
106
+ if (gridLines && gridLines.length > 0) {
107
+ try {
108
+ gridLines.forEach(line => fabricCanvas.bringToFront(line));
109
+ } catch (e) {
110
+ console.warn('bringGridToFront: Error bringing grid to front:', e);
111
+ }
112
+ }
113
+ };
114
+
115
+ /**
116
+ * send grid lines to back of the canvas
117
+ * @param fabricCanvas
118
+ * @param gridLines
119
+ */
120
+ export const sendGridToBack = (fabricCanvas: fabric.Canvas, gridLines: fabric.Object[]) => {
121
+ if (!fabricCanvas) {
122
+ console.warn('sendGridToBack: fabricCanvas is null or undefined');
123
+ return;
124
+ }
125
+
126
+ if (gridLines && gridLines.length > 0) {
127
+ try {
128
+ gridLines.forEach(line => fabricCanvas.sendToBack(line));
129
+ } catch (e) {
130
+ console.warn('sendGridToBack: Error sending grid to back:', e);
131
+ }
132
+ }
133
+ };
134
+
135
+ /**
136
+ * clear all objects except the grid on the canvas
137
+ * @param fabricCanvas
138
+ */
139
+ export const clearCanvasExceptGrid = (fabricCanvas: fabric.Canvas) => {
140
+ if (!fabricCanvas) {
141
+ console.warn('clearCanvasExceptGrid: fabricCanvas is null or undefined');
142
+ return;
143
+ }
144
+
145
+ try {
146
+ fabricCanvas.getObjects().forEach(obj => {
147
+ if (!obj.data?.isGrid) {
148
+ fabricCanvas.remove(obj);
149
+ }
150
+ });
151
+ } catch (e) {
152
+ console.warn('clearCanvasExceptGrid: Error clearing canvas:', e);
153
+ }
154
+ };