embroidery-qc-image 1.0.30 → 1.0.32
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/dist/components/EmbroideryQCImage.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +449 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +449 -42
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +6 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,6 +33,71 @@ function styleInject(css, ref) {
|
|
|
33
33
|
var css_248z = ".render-embroidery {\r\n display: inline-block;\r\n position: relative;\r\n width: 100%;\r\n max-width: 100%;\r\n}\r\n\r\n.render-embroidery-canvas {\r\n display: block;\r\n width: 100%;\r\n height: auto;\r\n image-rendering: high-quality;\r\n background: transparent;\r\n}\r\n";
|
|
34
34
|
styleInject(css_248z);
|
|
35
35
|
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// CONSTANTS
|
|
38
|
+
// ============================================================================
|
|
39
|
+
const COLOR_MAP = {
|
|
40
|
+
"Army (1394)": "#545541",
|
|
41
|
+
Army: "#545541",
|
|
42
|
+
"Black (8)": "#060608",
|
|
43
|
+
Black: "#060608",
|
|
44
|
+
"Bubblegum (1309)": "#E77B9F",
|
|
45
|
+
Bubblegum: "#E77B9F",
|
|
46
|
+
"Carolina Blue (1274)": "#608CC9",
|
|
47
|
+
"Carolina Blue": "#608CC9",
|
|
48
|
+
"Celadon (1098)": "#8EAD8D",
|
|
49
|
+
Celadon: "#8EAD8D",
|
|
50
|
+
"Coffee Bean (1145)": "#502B23",
|
|
51
|
+
"Coffee Bean": "#502B23",
|
|
52
|
+
"Daffodil (1180)": "#FBE30D",
|
|
53
|
+
Daffodil: "#FBE30D",
|
|
54
|
+
"Dark Gray (1131)": "#2E272E",
|
|
55
|
+
"Dark Gray": "#2E272E",
|
|
56
|
+
"Doe Skin Beige (1344)": "#AE9B8B",
|
|
57
|
+
"Doe Skin Beige": "#AE9B8B",
|
|
58
|
+
"Dusty Blue (1373)": "#7B90A9",
|
|
59
|
+
"Dusty Blue": "#7B90A9",
|
|
60
|
+
"Forest Green (1397)": "#073020",
|
|
61
|
+
"Forest Green": "#073020",
|
|
62
|
+
"Gold (1425)": "#D2920A",
|
|
63
|
+
Gold: "#D2920A",
|
|
64
|
+
"Gray (1118)": "#9999A3",
|
|
65
|
+
Gray: "#9999A3",
|
|
66
|
+
"Ivory (1072)": "#E3DAC9",
|
|
67
|
+
Ivory: "#E3DAC9",
|
|
68
|
+
"Lavender (1032)": "#9274B6",
|
|
69
|
+
Lavender: "#9274B6",
|
|
70
|
+
"Light Denim (1133)": "#366696",
|
|
71
|
+
"Light Denim": "#366696",
|
|
72
|
+
"Light Salmon (1018)": "#E0A793",
|
|
73
|
+
"Light Salmon": "#E0A793",
|
|
74
|
+
"Maroon (1374)": "#480C1C",
|
|
75
|
+
Maroon: "#480C1C",
|
|
76
|
+
"Navy Blue (1044)": "#04072A",
|
|
77
|
+
"Navy Blue": "#04072A",
|
|
78
|
+
"Olive Green (1157)": "#625E1F",
|
|
79
|
+
"Olive Green": "#625E1F",
|
|
80
|
+
"Orange (1278)": "#D45D03",
|
|
81
|
+
Orange: "#D45D03",
|
|
82
|
+
"Peach Blush (1053)": "#E2C0B6",
|
|
83
|
+
"Peach Blush": "#E2C0B6",
|
|
84
|
+
"Pink (1148)": "#EFAFBF",
|
|
85
|
+
Pink: "#EFAFBF",
|
|
86
|
+
"Purple (1412)": "#37196F",
|
|
87
|
+
Purple: "#37196F",
|
|
88
|
+
"Red (1037)": "#9D000B",
|
|
89
|
+
Red: "#9D000B",
|
|
90
|
+
"Silver Sage (1396)": "#424F45",
|
|
91
|
+
"Silver Sage": "#424F45",
|
|
92
|
+
"Summer Sky (1432)": "#65A8D2",
|
|
93
|
+
"Summer Sky": "#65A8D2",
|
|
94
|
+
"Terra Cotta (1477)": "#AE3111",
|
|
95
|
+
"Terra Cotta": "#AE3111",
|
|
96
|
+
"Sand (1055)": "#D2C2AB",
|
|
97
|
+
Sand: "#D2C2AB",
|
|
98
|
+
"White (9)": "#D8D7DC",
|
|
99
|
+
White: "#D8D7DC",
|
|
100
|
+
};
|
|
36
101
|
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
37
102
|
const DEFAULT_WARNING_COLOR = "#FF8C00";
|
|
38
103
|
const BASE_URLS = {
|
|
@@ -73,7 +138,7 @@ const loadFont = (fontName) => {
|
|
|
73
138
|
// Check if font is already loaded in document.fonts (browser cache)
|
|
74
139
|
const fontFaceSet = document.fonts;
|
|
75
140
|
for (const font of fontFaceSet) {
|
|
76
|
-
if (font.family === fontName && font.status ===
|
|
141
|
+
if (font.family === fontName && font.status === "loaded") {
|
|
77
142
|
return Promise.resolve();
|
|
78
143
|
}
|
|
79
144
|
}
|
|
@@ -193,7 +258,9 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
193
258
|
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
194
259
|
finalize();
|
|
195
260
|
};
|
|
196
|
-
const desiredSrc = attemptedProxy
|
|
261
|
+
const desiredSrc = attemptedProxy
|
|
262
|
+
? getProxyUrl(getResizeUrl(url))
|
|
263
|
+
: getResizeUrl(url);
|
|
197
264
|
if (target.src !== desiredSrc) {
|
|
198
265
|
target.src = desiredSrc;
|
|
199
266
|
}
|
|
@@ -206,22 +273,22 @@ const getResizeUrl = (url) => {
|
|
|
206
273
|
try {
|
|
207
274
|
const urlObj = new URL(url);
|
|
208
275
|
// Xử lý cdn.shopify.com
|
|
209
|
-
if (urlObj.hostname ===
|
|
276
|
+
if (urlObj.hostname === "cdn.shopify.com") {
|
|
210
277
|
// Set hoặc update query param width=400
|
|
211
|
-
urlObj.searchParams.set(
|
|
278
|
+
urlObj.searchParams.set("width", "400");
|
|
212
279
|
return urlObj.toString();
|
|
213
280
|
}
|
|
214
281
|
// Xử lý m.media-amazon.com
|
|
215
|
-
if (urlObj.hostname ===
|
|
282
|
+
if (urlObj.hostname === "m.media-amazon.com") {
|
|
216
283
|
const pathname = urlObj.pathname;
|
|
217
284
|
// Split pathname theo dấu /
|
|
218
|
-
const pathArr = pathname.split(
|
|
285
|
+
const pathArr = pathname.split("/");
|
|
219
286
|
// Lấy filename (phần cuối cùng)
|
|
220
287
|
const filename = pathArr[pathArr.length - 1];
|
|
221
288
|
// Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
|
|
222
|
-
const cleanedFilename = filename.replace(/\._.*_/g,
|
|
289
|
+
const cleanedFilename = filename.replace(/\._.*_/g, "");
|
|
223
290
|
// Split filename đã clean theo dấu .
|
|
224
|
-
const parts = cleanedFilename.split(
|
|
291
|
+
const parts = cleanedFilename.split(".");
|
|
225
292
|
if (parts.length >= 2) {
|
|
226
293
|
// Lấy phần đầu và phần cuối
|
|
227
294
|
const firstPart = parts[0];
|
|
@@ -231,16 +298,16 @@ const getResizeUrl = (url) => {
|
|
|
231
298
|
// Thay filename mới vào pathArr
|
|
232
299
|
pathArr[pathArr.length - 1] = newFilename;
|
|
233
300
|
// Join lại
|
|
234
|
-
urlObj.pathname = pathArr.join(
|
|
301
|
+
urlObj.pathname = pathArr.join("/");
|
|
235
302
|
return urlObj.toString();
|
|
236
303
|
}
|
|
237
304
|
}
|
|
238
305
|
// Xử lý i.etsystatic.com
|
|
239
|
-
if (urlObj.hostname ===
|
|
306
|
+
if (urlObj.hostname === "i.etsystatic.com") {
|
|
240
307
|
const pathname = urlObj.pathname;
|
|
241
308
|
// Thay il_fullxfull bằng il_400x400
|
|
242
|
-
if (pathname.includes(
|
|
243
|
-
const newPathname = pathname.replace(/il_fullxfull/g,
|
|
309
|
+
if (pathname.includes("il_fullxfull")) {
|
|
310
|
+
const newPathname = pathname.replace(/il_fullxfull/g, "il_400x400");
|
|
244
311
|
urlObj.pathname = newPathname;
|
|
245
312
|
return urlObj.toString();
|
|
246
313
|
}
|
|
@@ -364,7 +431,8 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight, mockupBounds = null) =>
|
|
|
364
431
|
let lineToRender = line;
|
|
365
432
|
if (ctx.measureText(line).width > effectiveMaxWidth) {
|
|
366
433
|
// Cắt từng ký tự cho đến khi vừa
|
|
367
|
-
while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
|
|
434
|
+
while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
|
|
435
|
+
lineToRender.length > 0) {
|
|
368
436
|
lineToRender = lineToRender.slice(0, -1);
|
|
369
437
|
}
|
|
370
438
|
}
|
|
@@ -535,6 +603,324 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
535
603
|
// ============================================================================
|
|
536
604
|
// RENDERING FUNCTIONS
|
|
537
605
|
// ============================================================================
|
|
606
|
+
const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
|
|
607
|
+
// Clear canvas
|
|
608
|
+
ctx.fillStyle = "#e7e7e7";
|
|
609
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
610
|
+
const padding = LAYOUT.PADDING * 6;
|
|
611
|
+
const usableWidth = canvas.width - padding * 2;
|
|
612
|
+
const usableHeight = canvas.height - padding * 2;
|
|
613
|
+
// Calculate sections
|
|
614
|
+
const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
|
|
615
|
+
const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
|
|
616
|
+
const topSectionY = padding;
|
|
617
|
+
const bottomSectionY = topSectionY + topSectionHeight;
|
|
618
|
+
// Get first side (stroke_patches should have only one side)
|
|
619
|
+
const side = config.sides[0];
|
|
620
|
+
if (!side || !side.positions.length) {
|
|
621
|
+
renderErrorState(ctx, canvas, "Không có dữ liệu positions");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const position = side.positions[0];
|
|
625
|
+
if (position.type !== "TEXT") {
|
|
626
|
+
renderErrorState(ctx, canvas, "Position phải là TEXT");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// Get layer colors with empty check (don't use fallback)
|
|
630
|
+
const textColor = position.layer_colors?.[0];
|
|
631
|
+
const borderColor = position.layer_colors?.[1];
|
|
632
|
+
const backgroundColor = position.layer_colors?.[2];
|
|
633
|
+
const fabricColor = position.layer_colors?.[3]; // Màu vải
|
|
634
|
+
// For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
|
|
635
|
+
const textColorForRender = textColor;
|
|
636
|
+
const borderColorForRender = borderColor;
|
|
637
|
+
const backgroundColorForRender = backgroundColor;
|
|
638
|
+
// Check if font is missing (but continue rendering)
|
|
639
|
+
const isFontMissing = !position.font || position.font.trim() === "";
|
|
640
|
+
// ============================================================================
|
|
641
|
+
// TOP SECTION (2/3): Hiển thị mẫu preview
|
|
642
|
+
// ============================================================================
|
|
643
|
+
ctx.save();
|
|
644
|
+
// Draw "Hình mẫu:" label at the top
|
|
645
|
+
const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
|
|
646
|
+
ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
647
|
+
ctx.fillStyle = "#CC0000"; // Red color
|
|
648
|
+
ctx.fillText("Hình mẫu:", padding, topSectionY);
|
|
649
|
+
// Adjust top section Y to account for title + extra spacing (40px)
|
|
650
|
+
const extraSpacing = 40;
|
|
651
|
+
const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
|
|
652
|
+
const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
|
|
653
|
+
// Calculate text size to fit top section
|
|
654
|
+
let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
|
|
655
|
+
const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
|
|
656
|
+
ctx.font = `${previewFontSize}px ${fontToUse}`;
|
|
657
|
+
const text = position.text || "";
|
|
658
|
+
const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
|
|
659
|
+
const maxTextHeight = actualTopSectionHeight; // Use 60% of height
|
|
660
|
+
// Scale down font size to fit
|
|
661
|
+
let textWidth = ctx.measureText(text).width;
|
|
662
|
+
while (textWidth > maxTextWidth && previewFontSize > 50) {
|
|
663
|
+
previewFontSize *= 0.95;
|
|
664
|
+
ctx.font = `${previewFontSize}px ${fontToUse}`;
|
|
665
|
+
textWidth = ctx.measureText(text).width;
|
|
666
|
+
}
|
|
667
|
+
// Ensure text height also fits
|
|
668
|
+
while (previewFontSize > maxTextHeight && previewFontSize > 50) {
|
|
669
|
+
previewFontSize *= 0.95;
|
|
670
|
+
ctx.font = `${previewFontSize}px ${fontToUse}`;
|
|
671
|
+
}
|
|
672
|
+
// Update textWidth after final scaling
|
|
673
|
+
textWidth = ctx.measureText(text).width;
|
|
674
|
+
// Center the text in top section
|
|
675
|
+
const textX = padding + usableWidth / 2 - textWidth / 2;
|
|
676
|
+
const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
|
|
677
|
+
// Get color hex values (use render colors with fallback)
|
|
678
|
+
const textColorHex = COLOR_MAP[textColorForRender] || LAYOUT.LABEL_COLOR;
|
|
679
|
+
const borderColorHex = COLOR_MAP[borderColorForRender] || LAYOUT.LABEL_COLOR;
|
|
680
|
+
const bgColorHex = COLOR_MAP[backgroundColorForRender] || "#FFFFFF";
|
|
681
|
+
// Calculate stroke widths
|
|
682
|
+
// Background needs to be MUCH thicker to create spacing from text
|
|
683
|
+
// Border needs to be even thicker to wrap around background
|
|
684
|
+
const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
|
|
685
|
+
const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
|
|
686
|
+
// Layer 1: Draw border stroke (outermost)
|
|
687
|
+
ctx.strokeStyle = borderColorHex;
|
|
688
|
+
ctx.lineWidth = borderWidth;
|
|
689
|
+
ctx.lineJoin = "round";
|
|
690
|
+
ctx.lineCap = "round";
|
|
691
|
+
ctx.strokeText(text, textX, textY);
|
|
692
|
+
// Layer 2: Draw background color stroke (middle - creates spacing from text)
|
|
693
|
+
ctx.strokeStyle = bgColorHex;
|
|
694
|
+
ctx.lineWidth = bgWidth;
|
|
695
|
+
ctx.lineJoin = "round";
|
|
696
|
+
ctx.lineCap = "round";
|
|
697
|
+
ctx.strokeText(text, textX, textY);
|
|
698
|
+
// Layer 3: Draw text fill ONLY (no stroke on text itself)
|
|
699
|
+
ctx.fillStyle = textColorHex;
|
|
700
|
+
ctx.fillText(text, textX, textY);
|
|
701
|
+
ctx.restore();
|
|
702
|
+
// ============================================================================
|
|
703
|
+
// BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
|
|
704
|
+
// ============================================================================
|
|
705
|
+
ctx.save();
|
|
706
|
+
// Draw border around bottom section for debugging
|
|
707
|
+
// ctx.strokeStyle = "#ccc";
|
|
708
|
+
// ctx.lineWidth = 2;
|
|
709
|
+
// ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
|
|
710
|
+
const bottomPadding = 0; // Không padding ngang, sát lề
|
|
711
|
+
const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
|
|
712
|
+
const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
|
|
713
|
+
// Split bottom section: 60% left for info, 40% right for image
|
|
714
|
+
const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
|
|
715
|
+
const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
|
|
716
|
+
const imageSectionX = padding + infoSectionWidth;
|
|
717
|
+
// Left side: Info list
|
|
718
|
+
const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
|
|
719
|
+
const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
|
|
720
|
+
let infoY = bottomSectionY + LAYOUT.PADDING;
|
|
721
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
722
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
723
|
+
ctx.textAlign = "left";
|
|
724
|
+
ctx.textBaseline = "top";
|
|
725
|
+
// Asterisk prefix style
|
|
726
|
+
const drawAsterisk = (x, y) => {
|
|
727
|
+
ctx.save();
|
|
728
|
+
ctx.fillStyle = "#CC0000"; // Red asterisk
|
|
729
|
+
ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
730
|
+
ctx.fillText("*", x, y);
|
|
731
|
+
ctx.restore();
|
|
732
|
+
};
|
|
733
|
+
const asteriskWidth = ctx.measureText("*").width + 5;
|
|
734
|
+
const startX = padding + asteriskWidth;
|
|
735
|
+
// Font - render "Font: " với font mặc định, tên font với font đó
|
|
736
|
+
drawAsterisk(padding, infoY);
|
|
737
|
+
const fontPrefix = "Font: ";
|
|
738
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
739
|
+
ctx.fillText(fontPrefix, startX, infoY);
|
|
740
|
+
if (isFontMissing) {
|
|
741
|
+
// Hiển thị warning màu đỏ nếu thiếu font
|
|
742
|
+
ctx.fillStyle = "#CC0000"; // Red color
|
|
743
|
+
ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
|
|
744
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Render font name với chính font đó
|
|
748
|
+
const prefixWidth = ctx.measureText(fontPrefix).width;
|
|
749
|
+
const fontName = position.font || LAYOUT.FONT_FAMILY;
|
|
750
|
+
ctx.font = `${infoFontSize}px ${fontName}`;
|
|
751
|
+
ctx.fillText(fontName, startX + prefixWidth, infoY);
|
|
752
|
+
}
|
|
753
|
+
infoY += infoLineHeight;
|
|
754
|
+
// Reset font về mặc định cho các dòng tiếp theo
|
|
755
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
756
|
+
// Màu chữ (Text Color) - layer_colors[0]
|
|
757
|
+
drawAsterisk(padding, infoY);
|
|
758
|
+
const textColorPrefix = "Màu chữ: ";
|
|
759
|
+
ctx.fillText(textColorPrefix, startX, infoY);
|
|
760
|
+
if (!textColor || textColor.trim() === "") {
|
|
761
|
+
// Hiển thị warning màu đỏ nếu thiếu màu
|
|
762
|
+
const prefixWidth = ctx.measureText(textColorPrefix).width;
|
|
763
|
+
ctx.fillStyle = "#CC0000";
|
|
764
|
+
ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
|
|
765
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
// Hiển thị tên màu
|
|
769
|
+
const prefixWidth = ctx.measureText(textColorPrefix).width;
|
|
770
|
+
ctx.fillText(textColor, startX + prefixWidth, infoY);
|
|
771
|
+
// Draw text color swatch
|
|
772
|
+
const swatchSize = infoFontSize * 1.3;
|
|
773
|
+
const swatchX = startX +
|
|
774
|
+
ctx.measureText(textColorPrefix + textColor).width +
|
|
775
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
776
|
+
const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
777
|
+
const textColorSwatchUrl = getImageUrl("threadColor", textColor);
|
|
778
|
+
const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
|
|
779
|
+
if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
|
|
780
|
+
const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
|
|
781
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
782
|
+
ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
infoY += infoLineHeight;
|
|
786
|
+
// Màu nền (Background Color) - layer_colors[2]
|
|
787
|
+
drawAsterisk(padding, infoY);
|
|
788
|
+
const bgColorPrefix = "Màu nền: ";
|
|
789
|
+
ctx.fillText(bgColorPrefix, startX, infoY);
|
|
790
|
+
if (!backgroundColor || backgroundColor.trim() === "") {
|
|
791
|
+
// Hiển thị warning màu đỏ nếu thiếu màu
|
|
792
|
+
const prefixWidth = ctx.measureText(bgColorPrefix).width;
|
|
793
|
+
ctx.fillStyle = "#CC0000";
|
|
794
|
+
ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
|
|
795
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
// Hiển thị tên màu
|
|
799
|
+
const prefixWidth = ctx.measureText(bgColorPrefix).width;
|
|
800
|
+
ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
|
|
801
|
+
// Draw background color swatch
|
|
802
|
+
const swatchSize = infoFontSize * 1.3;
|
|
803
|
+
const bgSwatchX = startX +
|
|
804
|
+
ctx.measureText(bgColorPrefix + backgroundColor).width +
|
|
805
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
806
|
+
const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
807
|
+
const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
|
|
808
|
+
const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
|
|
809
|
+
if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
|
|
810
|
+
const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
|
|
811
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
812
|
+
ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
infoY += infoLineHeight;
|
|
816
|
+
// Màu viền (Border Color) - layer_colors[1]
|
|
817
|
+
drawAsterisk(padding, infoY);
|
|
818
|
+
const borderColorPrefix = "Màu viền: ";
|
|
819
|
+
ctx.fillText(borderColorPrefix, startX, infoY);
|
|
820
|
+
if (!borderColor || borderColor.trim() === "") {
|
|
821
|
+
// Hiển thị warning màu đỏ nếu thiếu màu
|
|
822
|
+
const prefixWidth = ctx.measureText(borderColorPrefix).width;
|
|
823
|
+
ctx.fillStyle = "#CC0000";
|
|
824
|
+
ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
|
|
825
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
// Hiển thị tên màu
|
|
829
|
+
const prefixWidth = ctx.measureText(borderColorPrefix).width;
|
|
830
|
+
ctx.fillText(borderColor, startX + prefixWidth, infoY);
|
|
831
|
+
// Draw border color swatch
|
|
832
|
+
const swatchSize = infoFontSize * 1.3;
|
|
833
|
+
const borderSwatchX = startX +
|
|
834
|
+
ctx.measureText(borderColorPrefix + borderColor).width +
|
|
835
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
836
|
+
const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
837
|
+
const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
|
|
838
|
+
const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
|
|
839
|
+
if (borderColorSwatchImg?.complete &&
|
|
840
|
+
borderColorSwatchImg.naturalHeight > 0) {
|
|
841
|
+
const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
|
|
842
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
843
|
+
ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
infoY += infoLineHeight;
|
|
847
|
+
// Màu vải (Fabric Color) - layer_colors[3]
|
|
848
|
+
drawAsterisk(padding, infoY);
|
|
849
|
+
const fabricColorPrefix = "Màu vải: ";
|
|
850
|
+
ctx.fillText(fabricColorPrefix, startX, infoY);
|
|
851
|
+
if (!fabricColor || fabricColor.trim() === "") {
|
|
852
|
+
// Hiển thị warning màu đỏ nếu thiếu màu
|
|
853
|
+
const prefixWidth = ctx.measureText(fabricColorPrefix).width;
|
|
854
|
+
ctx.fillStyle = "#CC0000";
|
|
855
|
+
ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
|
|
856
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
// Hiển thị tên màu
|
|
860
|
+
const prefixWidth = ctx.measureText(fabricColorPrefix).width;
|
|
861
|
+
ctx.fillText(fabricColor, startX + prefixWidth, infoY);
|
|
862
|
+
// Draw fabric color swatch
|
|
863
|
+
const swatchSize = infoFontSize * 1.3;
|
|
864
|
+
const fabricSwatchX = startX +
|
|
865
|
+
ctx.measureText(fabricColorPrefix + fabricColor).width +
|
|
866
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
867
|
+
const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
868
|
+
const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
|
|
869
|
+
const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
|
|
870
|
+
if (fabricColorSwatchImg?.complete &&
|
|
871
|
+
fabricColorSwatchImg.naturalHeight > 0) {
|
|
872
|
+
const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
|
|
873
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
874
|
+
ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
infoY += infoLineHeight;
|
|
878
|
+
// Attachment
|
|
879
|
+
if (position.attachment) {
|
|
880
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
881
|
+
const attachmentLabel = `Attachment: ${position.attachment}`;
|
|
882
|
+
ctx.fillText(attachmentLabel, startX, infoY);
|
|
883
|
+
infoY += infoLineHeight;
|
|
884
|
+
}
|
|
885
|
+
// Size
|
|
886
|
+
if (side.size) {
|
|
887
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
888
|
+
const sizeLabel = `Size: ${side.size}`;
|
|
889
|
+
ctx.fillText(sizeLabel, startX, infoY);
|
|
890
|
+
infoY += infoLineHeight;
|
|
891
|
+
}
|
|
892
|
+
// Right side: Image from config.image_url
|
|
893
|
+
if (config.image_url) {
|
|
894
|
+
// Draw "Mockup" label
|
|
895
|
+
ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
|
|
896
|
+
ctx.fillStyle = "#000000";
|
|
897
|
+
const mockupLabel = "Mockup";
|
|
898
|
+
const mockupLabelWidth = ctx.measureText(mockupLabel).width;
|
|
899
|
+
const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
|
|
900
|
+
ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
|
|
901
|
+
const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
|
|
902
|
+
const img = imageRefs.current.get(config.image_url) ??
|
|
903
|
+
imageRefs.current.get("mockup");
|
|
904
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
905
|
+
const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
|
|
906
|
+
const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
|
|
907
|
+
const imgAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
908
|
+
let drawWidth = maxImgWidth;
|
|
909
|
+
let drawHeight = drawWidth / imgAspectRatio;
|
|
910
|
+
if (drawHeight > maxImgHeight) {
|
|
911
|
+
drawHeight = maxImgHeight;
|
|
912
|
+
drawWidth = drawHeight * imgAspectRatio;
|
|
913
|
+
}
|
|
914
|
+
const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
|
|
915
|
+
const imgY = bottomSectionY +
|
|
916
|
+
LAYOUT.PADDING +
|
|
917
|
+
mockupLabelHeight +
|
|
918
|
+
(bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
|
|
919
|
+
ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
ctx.restore();
|
|
923
|
+
};
|
|
538
924
|
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
539
925
|
const ctx = canvas.getContext("2d");
|
|
540
926
|
if (!ctx)
|
|
@@ -549,6 +935,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
549
935
|
}
|
|
550
936
|
if (!config.sides?.length)
|
|
551
937
|
return;
|
|
938
|
+
// Check if this is a stroke_patches layout
|
|
939
|
+
const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
|
|
940
|
+
if (hasStrokePatches) {
|
|
941
|
+
renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
552
944
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
553
945
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
554
946
|
if (config.image_url) {
|
|
@@ -721,7 +1113,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
|
|
|
721
1113
|
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
722
1114
|
const lineTopY = y;
|
|
723
1115
|
const lineBottomY = y + lineHeight;
|
|
724
|
-
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1116
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1117
|
+
lineBottomY > mockupBounds.y;
|
|
725
1118
|
if (overlapsY) {
|
|
726
1119
|
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
727
1120
|
const maxAllowedWidth = mockupBounds.x - x;
|
|
@@ -768,15 +1161,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
768
1161
|
const iconPositions = side.positions.filter((p) => p.type === "ICON");
|
|
769
1162
|
// Kiểm tra tất cả TEXT positions có trống không
|
|
770
1163
|
// Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
|
|
771
|
-
const allTextEmpty = textPositions.length === 0 ||
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1164
|
+
const allTextEmpty = textPositions.length === 0 ||
|
|
1165
|
+
textPositions.every((p) => {
|
|
1166
|
+
const text = p.text ?? "";
|
|
1167
|
+
return text.trim() === "";
|
|
1168
|
+
});
|
|
775
1169
|
// Kiểm tra tất cả ICON positions có is_delete_icon = true không
|
|
776
1170
|
// Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
|
|
777
|
-
const allIconsDeleted = iconPositions.length === 0 ||
|
|
778
|
-
|
|
779
|
-
|
|
1171
|
+
const allIconsDeleted = iconPositions.length === 0 ||
|
|
1172
|
+
iconPositions.every((p) => {
|
|
1173
|
+
return p.is_delete_icon === true;
|
|
1174
|
+
});
|
|
780
1175
|
// Nếu tất cả TEXT trống và tất cả ICON bị xóa, chỉ render dòng "(không thêu gì)"
|
|
781
1176
|
if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
|
|
782
1177
|
ctx.save();
|
|
@@ -790,7 +1185,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
790
1185
|
return currentY - startY;
|
|
791
1186
|
}
|
|
792
1187
|
// Compute uniform properties
|
|
793
|
-
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1188
|
+
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1189
|
+
(!p.layer_colors?.length || p.layer_colors.length === 1));
|
|
794
1190
|
const iconColorValues = iconColorPositions
|
|
795
1191
|
.map((p) => {
|
|
796
1192
|
if (p.layer_colors?.length === 1)
|
|
@@ -912,7 +1308,8 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
912
1308
|
...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
|
|
913
1309
|
];
|
|
914
1310
|
if (textPositions.length === 0 &&
|
|
915
|
-
(!options?.additionalColorValues ||
|
|
1311
|
+
(!options?.additionalColorValues ||
|
|
1312
|
+
options.additionalColorValues.length === 0)) {
|
|
916
1313
|
return defaults;
|
|
917
1314
|
}
|
|
918
1315
|
const colors = new Set(colorSources);
|
|
@@ -1013,7 +1410,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1013
1410
|
}
|
|
1014
1411
|
rendered++;
|
|
1015
1412
|
}
|
|
1016
|
-
if (values.floral &&
|
|
1413
|
+
if (values.floral &&
|
|
1414
|
+
values.floral !== "None" &&
|
|
1415
|
+
shouldRenderField("floral")) {
|
|
1017
1416
|
const floralUrl = getImageUrl("floral", values.floral);
|
|
1018
1417
|
const floralImg = imageRefs.current.get(floralUrl);
|
|
1019
1418
|
// Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
|
|
@@ -1077,9 +1476,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1077
1476
|
let currentY = y;
|
|
1078
1477
|
// Chuẩn hóa xuống dòng:
|
|
1079
1478
|
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1080
|
-
const normalizeNewlines = (text) => text
|
|
1081
|
-
.replace(/\r\n/g, "\n")
|
|
1082
|
-
.replace(/\r/g, "\n");
|
|
1479
|
+
const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1083
1480
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1084
1481
|
const rawOriginalText = position.text ?? "";
|
|
1085
1482
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1116,7 +1513,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1116
1513
|
let minShrinkRatio = 1;
|
|
1117
1514
|
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1118
1515
|
lines.forEach((line, idx) => {
|
|
1119
|
-
const lineY = textCenterY -
|
|
1516
|
+
const lineY = textCenterY -
|
|
1517
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1518
|
+
idx * valueLineHeight;
|
|
1120
1519
|
// Tính effectiveMaxWidth cho dòng này
|
|
1121
1520
|
const effectiveMaxWidth = mockupBounds
|
|
1122
1521
|
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
@@ -1144,7 +1543,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1144
1543
|
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1145
1544
|
// Font size đã được tính để vừa với effectiveMaxWidth của từng dòng, nên không cần cắt text
|
|
1146
1545
|
lines.forEach((line, idx) => {
|
|
1147
|
-
const lineY = textCenterY -
|
|
1546
|
+
const lineY = textCenterY -
|
|
1547
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1548
|
+
idx * valueLineHeight;
|
|
1148
1549
|
ctx.fillText(line, valueStartX, lineY);
|
|
1149
1550
|
});
|
|
1150
1551
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
@@ -1231,7 +1632,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1231
1632
|
let lineToRender = line;
|
|
1232
1633
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1233
1634
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1234
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1635
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1636
|
+
lineToRender.length > 0) {
|
|
1235
1637
|
lineToRender = lineToRender.slice(0, -1);
|
|
1236
1638
|
}
|
|
1237
1639
|
}
|
|
@@ -1256,7 +1658,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1256
1658
|
if (totalWidth > shrunkEffectiveMaxWidth) {
|
|
1257
1659
|
// Cần cắt font name
|
|
1258
1660
|
let truncatedFontName = fontName;
|
|
1259
|
-
while (ctx.measureText(truncatedFontName).width >
|
|
1661
|
+
while (ctx.measureText(truncatedFontName).width >
|
|
1662
|
+
shrunkEffectiveMaxWidth - prefixWidth &&
|
|
1663
|
+
truncatedFontName.length > 0) {
|
|
1260
1664
|
truncatedFontName = truncatedFontName.slice(0, -1);
|
|
1261
1665
|
}
|
|
1262
1666
|
ctx.fillText(truncatedFontName, currentX, currentY);
|
|
@@ -1270,7 +1674,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1270
1674
|
const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
|
|
1271
1675
|
if (remainingWidth > 0) {
|
|
1272
1676
|
let truncatedSuffix = suffix;
|
|
1273
|
-
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1677
|
+
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1678
|
+
truncatedSuffix.length > 0) {
|
|
1274
1679
|
truncatedSuffix = truncatedSuffix.slice(0, -1);
|
|
1275
1680
|
}
|
|
1276
1681
|
if (truncatedSuffix.length > 0) {
|
|
@@ -1311,7 +1716,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1311
1716
|
else {
|
|
1312
1717
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1313
1718
|
swatchX = swatchesStartX;
|
|
1314
|
-
swatchY =
|
|
1719
|
+
swatchY =
|
|
1720
|
+
result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1315
1721
|
currentY += result.height;
|
|
1316
1722
|
}
|
|
1317
1723
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -1420,7 +1826,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1420
1826
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1421
1827
|
const iconHeight = iconFontSize * 2;
|
|
1422
1828
|
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1423
|
-
iconImageReservedWidth =
|
|
1829
|
+
iconImageReservedWidth =
|
|
1830
|
+
iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1424
1831
|
}
|
|
1425
1832
|
}
|
|
1426
1833
|
}
|
|
@@ -1498,7 +1905,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1498
1905
|
let lineToRender = line;
|
|
1499
1906
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1500
1907
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1501
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1908
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1909
|
+
lineToRender.length > 0) {
|
|
1502
1910
|
lineToRender = lineToRender.slice(0, -1);
|
|
1503
1911
|
}
|
|
1504
1912
|
}
|
|
@@ -1519,7 +1927,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1519
1927
|
let lineToRender = lineText;
|
|
1520
1928
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1521
1929
|
if (lineWidth > shrunkEffectiveMaxWidth) {
|
|
1522
|
-
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1930
|
+
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1931
|
+
lineToRender.length > 0) {
|
|
1523
1932
|
lineToRender = lineToRender.slice(0, -1);
|
|
1524
1933
|
}
|
|
1525
1934
|
}
|
|
@@ -1582,7 +1991,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1582
1991
|
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1583
1992
|
testLines.length * lineHeight;
|
|
1584
1993
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1585
|
-
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1994
|
+
const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
|
|
1586
1995
|
const textEndX = x + testTextWidth;
|
|
1587
1996
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1588
1997
|
const swatchesStartX = textEndX + spacing;
|
|
@@ -1611,7 +2020,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1611
2020
|
else {
|
|
1612
2021
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1613
2022
|
swatchX = swatchesStartX;
|
|
1614
|
-
swatchY =
|
|
2023
|
+
swatchY =
|
|
2024
|
+
colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1615
2025
|
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1616
2026
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1617
2027
|
// cursorY đã được cập nhật ở trên
|
|
@@ -1627,10 +2037,7 @@ const prepareExportCanvas = async (config, options = {}) => {
|
|
|
1627
2037
|
current: new Map(),
|
|
1628
2038
|
};
|
|
1629
2039
|
// Load fonts and images in parallel
|
|
1630
|
-
await Promise.all([
|
|
1631
|
-
preloadFonts(config),
|
|
1632
|
-
preloadImages(config, imageRefs)
|
|
1633
|
-
]);
|
|
2040
|
+
await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
|
|
1634
2041
|
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
1635
2042
|
if (!canvas.width || !canvas.height) {
|
|
1636
2043
|
return null;
|