embroidery-qc-image 1.0.30 → 1.0.31
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 +387 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +387 -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,262 @@ 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
|
+
// Validate required fields
|
|
630
|
+
if (!position.font) {
|
|
631
|
+
renderErrorState(ctx, canvas, "Thiếu font");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Validate layer_colors
|
|
635
|
+
if (!position.layer_colors) {
|
|
636
|
+
renderErrorState(ctx, canvas, "Thiếu layer_colors");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (position.layer_colors.length !== 3) {
|
|
640
|
+
renderErrorState(ctx, canvas, `layer_colors phải có đúng 3 phần tử, hiện tại có ${position.layer_colors.length}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Validate từng màu
|
|
644
|
+
const [textColor, borderColor, backgroundColor] = position.layer_colors;
|
|
645
|
+
const missingColors = [];
|
|
646
|
+
if (!textColor || textColor.trim() === "") {
|
|
647
|
+
missingColors.push("Màu text (vị trí 1)");
|
|
648
|
+
}
|
|
649
|
+
if (!borderColor || borderColor.trim() === "") {
|
|
650
|
+
missingColors.push("Màu border (vị trí 2)");
|
|
651
|
+
}
|
|
652
|
+
if (!backgroundColor || backgroundColor.trim() === "") {
|
|
653
|
+
missingColors.push("Màu background (vị trí 3)");
|
|
654
|
+
}
|
|
655
|
+
if (missingColors.length > 0) {
|
|
656
|
+
renderErrorState(ctx, canvas, `Thiếu màu trong layer_colors:\n${missingColors.join("\n")}`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
// ============================================================================
|
|
660
|
+
// TOP SECTION (2/3): Hiển thị mẫu preview
|
|
661
|
+
// ============================================================================
|
|
662
|
+
ctx.save();
|
|
663
|
+
// Draw "Hình mẫu:" label at the top
|
|
664
|
+
const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
|
|
665
|
+
ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
666
|
+
ctx.fillStyle = "#CC0000"; // Red color
|
|
667
|
+
ctx.fillText("Hình mẫu:", padding, topSectionY);
|
|
668
|
+
// Adjust top section Y to account for title + extra spacing (40px)
|
|
669
|
+
const extraSpacing = 40;
|
|
670
|
+
const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
|
|
671
|
+
const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
|
|
672
|
+
// Calculate text size to fit top section
|
|
673
|
+
let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
|
|
674
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
675
|
+
const text = position.text || "";
|
|
676
|
+
const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
|
|
677
|
+
const maxTextHeight = actualTopSectionHeight; // Use 60% of height
|
|
678
|
+
// Scale down font size to fit
|
|
679
|
+
let textWidth = ctx.measureText(text).width;
|
|
680
|
+
while (textWidth > maxTextWidth && previewFontSize > 50) {
|
|
681
|
+
previewFontSize *= 0.95;
|
|
682
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
683
|
+
textWidth = ctx.measureText(text).width;
|
|
684
|
+
}
|
|
685
|
+
// Ensure text height also fits
|
|
686
|
+
while (previewFontSize > maxTextHeight && previewFontSize > 50) {
|
|
687
|
+
previewFontSize *= 0.95;
|
|
688
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
689
|
+
}
|
|
690
|
+
// Update textWidth after final scaling
|
|
691
|
+
textWidth = ctx.measureText(text).width;
|
|
692
|
+
// Center the text in top section
|
|
693
|
+
const textX = padding + usableWidth / 2 - textWidth / 2;
|
|
694
|
+
const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
|
|
695
|
+
// Get color hex values
|
|
696
|
+
const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
|
|
697
|
+
const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
|
|
698
|
+
const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
|
|
699
|
+
// Calculate stroke widths
|
|
700
|
+
// Background needs to be MUCH thicker to create spacing from text
|
|
701
|
+
// Border needs to be even thicker to wrap around background
|
|
702
|
+
const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
|
|
703
|
+
const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
|
|
704
|
+
// Layer 1: Draw border stroke (outermost)
|
|
705
|
+
ctx.strokeStyle = borderColorHex;
|
|
706
|
+
ctx.lineWidth = borderWidth;
|
|
707
|
+
ctx.lineJoin = "round";
|
|
708
|
+
ctx.lineCap = "round";
|
|
709
|
+
ctx.strokeText(text, textX, textY);
|
|
710
|
+
// Layer 2: Draw background color stroke (middle - creates spacing from text)
|
|
711
|
+
ctx.strokeStyle = bgColorHex;
|
|
712
|
+
ctx.lineWidth = bgWidth;
|
|
713
|
+
ctx.lineJoin = "round";
|
|
714
|
+
ctx.lineCap = "round";
|
|
715
|
+
ctx.strokeText(text, textX, textY);
|
|
716
|
+
// Layer 3: Draw text fill ONLY (no stroke on text itself)
|
|
717
|
+
ctx.fillStyle = textColorHex;
|
|
718
|
+
ctx.fillText(text, textX, textY);
|
|
719
|
+
ctx.restore();
|
|
720
|
+
// ============================================================================
|
|
721
|
+
// BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
|
|
722
|
+
// ============================================================================
|
|
723
|
+
ctx.save();
|
|
724
|
+
// Draw border around bottom section for debugging
|
|
725
|
+
// ctx.strokeStyle = "#ccc";
|
|
726
|
+
// ctx.lineWidth = 2;
|
|
727
|
+
// ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
|
|
728
|
+
const bottomPadding = 0; // Không padding ngang, sát lề
|
|
729
|
+
const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
|
|
730
|
+
const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
|
|
731
|
+
// Split bottom section: 60% left for info, 40% right for image
|
|
732
|
+
const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
|
|
733
|
+
const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
|
|
734
|
+
const imageSectionX = padding + infoSectionWidth;
|
|
735
|
+
// Left side: Info list
|
|
736
|
+
const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
|
|
737
|
+
const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
|
|
738
|
+
let infoY = bottomSectionY + LAYOUT.PADDING;
|
|
739
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
740
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
741
|
+
ctx.textAlign = "left";
|
|
742
|
+
ctx.textBaseline = "top";
|
|
743
|
+
// Asterisk prefix style
|
|
744
|
+
const drawAsterisk = (x, y) => {
|
|
745
|
+
ctx.save();
|
|
746
|
+
ctx.fillStyle = "#CC0000"; // Red asterisk
|
|
747
|
+
ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
748
|
+
ctx.fillText("*", x, y);
|
|
749
|
+
ctx.restore();
|
|
750
|
+
};
|
|
751
|
+
const asteriskWidth = ctx.measureText("*").width + 5;
|
|
752
|
+
const startX = padding + asteriskWidth;
|
|
753
|
+
// Font - render "Font: " với font mặc định, tên font với font đó
|
|
754
|
+
drawAsterisk(padding, infoY);
|
|
755
|
+
const fontPrefix = "Font: ";
|
|
756
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
757
|
+
ctx.fillText(fontPrefix, startX, infoY);
|
|
758
|
+
// Render font name với chính font đó
|
|
759
|
+
const prefixWidth = ctx.measureText(fontPrefix).width;
|
|
760
|
+
ctx.font = `${infoFontSize}px ${position.font}`;
|
|
761
|
+
ctx.fillText(position.font, startX + prefixWidth, infoY);
|
|
762
|
+
infoY += infoLineHeight;
|
|
763
|
+
// Reset font về mặc định cho các dòng tiếp theo
|
|
764
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
765
|
+
// Màu chữ (Text Color)
|
|
766
|
+
drawAsterisk(padding, infoY);
|
|
767
|
+
const textColorLabel = `Màu chữ: ${textColor}`;
|
|
768
|
+
ctx.fillText(textColorLabel, startX, infoY);
|
|
769
|
+
// Draw text color swatch
|
|
770
|
+
const swatchSize = infoFontSize * 1.3; // Giảm từ 1.5 xuống 1.3
|
|
771
|
+
const swatchX = startX +
|
|
772
|
+
ctx.measureText(textColorLabel).width +
|
|
773
|
+
LAYOUT.ELEMENT_SPACING * 0.3; // Giảm spacing
|
|
774
|
+
const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
775
|
+
const textColorSwatchUrl = getImageUrl("threadColor", textColor);
|
|
776
|
+
const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
|
|
777
|
+
if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
|
|
778
|
+
const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
|
|
779
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
780
|
+
ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
|
|
781
|
+
}
|
|
782
|
+
infoY += infoLineHeight;
|
|
783
|
+
// Màu nền (Background Color)
|
|
784
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
785
|
+
const bgColorLabel = `Màu nền: ${backgroundColor}`;
|
|
786
|
+
ctx.fillText(bgColorLabel, startX, infoY);
|
|
787
|
+
// Draw background color swatch
|
|
788
|
+
const bgSwatchX = startX + ctx.measureText(bgColorLabel).width + LAYOUT.ELEMENT_SPACING * 0.3;
|
|
789
|
+
const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
790
|
+
const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
|
|
791
|
+
const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
|
|
792
|
+
if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
|
|
793
|
+
const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
|
|
794
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
795
|
+
ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
|
|
796
|
+
}
|
|
797
|
+
infoY += infoLineHeight;
|
|
798
|
+
// Màu viền (Border Color)
|
|
799
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
800
|
+
const borderColorLabel = `Màu viền: ${borderColor}`;
|
|
801
|
+
ctx.fillText(borderColorLabel, startX, infoY);
|
|
802
|
+
// Draw border color swatch
|
|
803
|
+
const borderSwatchX = startX +
|
|
804
|
+
ctx.measureText(borderColorLabel).width +
|
|
805
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
806
|
+
const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
807
|
+
const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
|
|
808
|
+
const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
|
|
809
|
+
if (borderColorSwatchImg?.complete &&
|
|
810
|
+
borderColorSwatchImg.naturalHeight > 0) {
|
|
811
|
+
const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
|
|
812
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
813
|
+
ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
|
|
814
|
+
}
|
|
815
|
+
infoY += infoLineHeight;
|
|
816
|
+
// Attachment
|
|
817
|
+
if (position.attachment) {
|
|
818
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
819
|
+
const attachmentLabel = `Attachment: ${position.attachment}`;
|
|
820
|
+
ctx.fillText(attachmentLabel, startX, infoY);
|
|
821
|
+
infoY += infoLineHeight;
|
|
822
|
+
}
|
|
823
|
+
// Size
|
|
824
|
+
if (side.size) {
|
|
825
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
826
|
+
const sizeLabel = `Size: ${side.size}`;
|
|
827
|
+
ctx.fillText(sizeLabel, startX, infoY);
|
|
828
|
+
infoY += infoLineHeight;
|
|
829
|
+
}
|
|
830
|
+
// Right side: Image from config.image_url
|
|
831
|
+
if (config.image_url) {
|
|
832
|
+
// Draw "Mockup" label
|
|
833
|
+
ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
|
|
834
|
+
ctx.fillStyle = "#000000";
|
|
835
|
+
const mockupLabel = "Mockup";
|
|
836
|
+
const mockupLabelWidth = ctx.measureText(mockupLabel).width;
|
|
837
|
+
const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
|
|
838
|
+
ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
|
|
839
|
+
const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
|
|
840
|
+
const img = imageRefs.current.get(config.image_url) ??
|
|
841
|
+
imageRefs.current.get("mockup");
|
|
842
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
843
|
+
const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
|
|
844
|
+
const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
|
|
845
|
+
const imgAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
846
|
+
let drawWidth = maxImgWidth;
|
|
847
|
+
let drawHeight = drawWidth / imgAspectRatio;
|
|
848
|
+
if (drawHeight > maxImgHeight) {
|
|
849
|
+
drawHeight = maxImgHeight;
|
|
850
|
+
drawWidth = drawHeight * imgAspectRatio;
|
|
851
|
+
}
|
|
852
|
+
const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
|
|
853
|
+
const imgY = bottomSectionY +
|
|
854
|
+
LAYOUT.PADDING +
|
|
855
|
+
mockupLabelHeight +
|
|
856
|
+
(bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
|
|
857
|
+
ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
ctx.restore();
|
|
861
|
+
};
|
|
538
862
|
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
539
863
|
const ctx = canvas.getContext("2d");
|
|
540
864
|
if (!ctx)
|
|
@@ -549,6 +873,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
549
873
|
}
|
|
550
874
|
if (!config.sides?.length)
|
|
551
875
|
return;
|
|
876
|
+
// Check if this is a stroke_patches layout
|
|
877
|
+
const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
|
|
878
|
+
if (hasStrokePatches) {
|
|
879
|
+
renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
552
882
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
553
883
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
554
884
|
if (config.image_url) {
|
|
@@ -721,7 +1051,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
|
|
|
721
1051
|
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
722
1052
|
const lineTopY = y;
|
|
723
1053
|
const lineBottomY = y + lineHeight;
|
|
724
|
-
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1054
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1055
|
+
lineBottomY > mockupBounds.y;
|
|
725
1056
|
if (overlapsY) {
|
|
726
1057
|
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
727
1058
|
const maxAllowedWidth = mockupBounds.x - x;
|
|
@@ -768,15 +1099,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
768
1099
|
const iconPositions = side.positions.filter((p) => p.type === "ICON");
|
|
769
1100
|
// Kiểm tra tất cả TEXT positions có trống không
|
|
770
1101
|
// 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
|
-
|
|
1102
|
+
const allTextEmpty = textPositions.length === 0 ||
|
|
1103
|
+
textPositions.every((p) => {
|
|
1104
|
+
const text = p.text ?? "";
|
|
1105
|
+
return text.trim() === "";
|
|
1106
|
+
});
|
|
775
1107
|
// Kiểm tra tất cả ICON positions có is_delete_icon = true không
|
|
776
1108
|
// 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
|
-
|
|
1109
|
+
const allIconsDeleted = iconPositions.length === 0 ||
|
|
1110
|
+
iconPositions.every((p) => {
|
|
1111
|
+
return p.is_delete_icon === true;
|
|
1112
|
+
});
|
|
780
1113
|
// 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
1114
|
if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
|
|
782
1115
|
ctx.save();
|
|
@@ -790,7 +1123,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
790
1123
|
return currentY - startY;
|
|
791
1124
|
}
|
|
792
1125
|
// Compute uniform properties
|
|
793
|
-
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1126
|
+
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1127
|
+
(!p.layer_colors?.length || p.layer_colors.length === 1));
|
|
794
1128
|
const iconColorValues = iconColorPositions
|
|
795
1129
|
.map((p) => {
|
|
796
1130
|
if (p.layer_colors?.length === 1)
|
|
@@ -912,7 +1246,8 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
912
1246
|
...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
|
|
913
1247
|
];
|
|
914
1248
|
if (textPositions.length === 0 &&
|
|
915
|
-
(!options?.additionalColorValues ||
|
|
1249
|
+
(!options?.additionalColorValues ||
|
|
1250
|
+
options.additionalColorValues.length === 0)) {
|
|
916
1251
|
return defaults;
|
|
917
1252
|
}
|
|
918
1253
|
const colors = new Set(colorSources);
|
|
@@ -1013,7 +1348,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1013
1348
|
}
|
|
1014
1349
|
rendered++;
|
|
1015
1350
|
}
|
|
1016
|
-
if (values.floral &&
|
|
1351
|
+
if (values.floral &&
|
|
1352
|
+
values.floral !== "None" &&
|
|
1353
|
+
shouldRenderField("floral")) {
|
|
1017
1354
|
const floralUrl = getImageUrl("floral", values.floral);
|
|
1018
1355
|
const floralImg = imageRefs.current.get(floralUrl);
|
|
1019
1356
|
// Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
|
|
@@ -1077,9 +1414,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1077
1414
|
let currentY = y;
|
|
1078
1415
|
// Chuẩn hóa xuống dòng:
|
|
1079
1416
|
// - 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");
|
|
1417
|
+
const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1083
1418
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1084
1419
|
const rawOriginalText = position.text ?? "";
|
|
1085
1420
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1116,7 +1451,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1116
1451
|
let minShrinkRatio = 1;
|
|
1117
1452
|
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1118
1453
|
lines.forEach((line, idx) => {
|
|
1119
|
-
const lineY = textCenterY -
|
|
1454
|
+
const lineY = textCenterY -
|
|
1455
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1456
|
+
idx * valueLineHeight;
|
|
1120
1457
|
// Tính effectiveMaxWidth cho dòng này
|
|
1121
1458
|
const effectiveMaxWidth = mockupBounds
|
|
1122
1459
|
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
@@ -1144,7 +1481,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1144
1481
|
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1145
1482
|
// 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
1483
|
lines.forEach((line, idx) => {
|
|
1147
|
-
const lineY = textCenterY -
|
|
1484
|
+
const lineY = textCenterY -
|
|
1485
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1486
|
+
idx * valueLineHeight;
|
|
1148
1487
|
ctx.fillText(line, valueStartX, lineY);
|
|
1149
1488
|
});
|
|
1150
1489
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
@@ -1231,7 +1570,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1231
1570
|
let lineToRender = line;
|
|
1232
1571
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1233
1572
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1234
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1573
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1574
|
+
lineToRender.length > 0) {
|
|
1235
1575
|
lineToRender = lineToRender.slice(0, -1);
|
|
1236
1576
|
}
|
|
1237
1577
|
}
|
|
@@ -1256,7 +1596,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1256
1596
|
if (totalWidth > shrunkEffectiveMaxWidth) {
|
|
1257
1597
|
// Cần cắt font name
|
|
1258
1598
|
let truncatedFontName = fontName;
|
|
1259
|
-
while (ctx.measureText(truncatedFontName).width >
|
|
1599
|
+
while (ctx.measureText(truncatedFontName).width >
|
|
1600
|
+
shrunkEffectiveMaxWidth - prefixWidth &&
|
|
1601
|
+
truncatedFontName.length > 0) {
|
|
1260
1602
|
truncatedFontName = truncatedFontName.slice(0, -1);
|
|
1261
1603
|
}
|
|
1262
1604
|
ctx.fillText(truncatedFontName, currentX, currentY);
|
|
@@ -1270,7 +1612,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1270
1612
|
const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
|
|
1271
1613
|
if (remainingWidth > 0) {
|
|
1272
1614
|
let truncatedSuffix = suffix;
|
|
1273
|
-
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1615
|
+
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1616
|
+
truncatedSuffix.length > 0) {
|
|
1274
1617
|
truncatedSuffix = truncatedSuffix.slice(0, -1);
|
|
1275
1618
|
}
|
|
1276
1619
|
if (truncatedSuffix.length > 0) {
|
|
@@ -1311,7 +1654,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1311
1654
|
else {
|
|
1312
1655
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1313
1656
|
swatchX = swatchesStartX;
|
|
1314
|
-
swatchY =
|
|
1657
|
+
swatchY =
|
|
1658
|
+
result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1315
1659
|
currentY += result.height;
|
|
1316
1660
|
}
|
|
1317
1661
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -1420,7 +1764,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1420
1764
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1421
1765
|
const iconHeight = iconFontSize * 2;
|
|
1422
1766
|
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1423
|
-
iconImageReservedWidth =
|
|
1767
|
+
iconImageReservedWidth =
|
|
1768
|
+
iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1424
1769
|
}
|
|
1425
1770
|
}
|
|
1426
1771
|
}
|
|
@@ -1498,7 +1843,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1498
1843
|
let lineToRender = line;
|
|
1499
1844
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1500
1845
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1501
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1846
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1847
|
+
lineToRender.length > 0) {
|
|
1502
1848
|
lineToRender = lineToRender.slice(0, -1);
|
|
1503
1849
|
}
|
|
1504
1850
|
}
|
|
@@ -1519,7 +1865,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1519
1865
|
let lineToRender = lineText;
|
|
1520
1866
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1521
1867
|
if (lineWidth > shrunkEffectiveMaxWidth) {
|
|
1522
|
-
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1868
|
+
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1869
|
+
lineToRender.length > 0) {
|
|
1523
1870
|
lineToRender = lineToRender.slice(0, -1);
|
|
1524
1871
|
}
|
|
1525
1872
|
}
|
|
@@ -1582,7 +1929,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1582
1929
|
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1583
1930
|
testLines.length * lineHeight;
|
|
1584
1931
|
// 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));
|
|
1932
|
+
const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
|
|
1586
1933
|
const textEndX = x + testTextWidth;
|
|
1587
1934
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1588
1935
|
const swatchesStartX = textEndX + spacing;
|
|
@@ -1611,7 +1958,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1611
1958
|
else {
|
|
1612
1959
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1613
1960
|
swatchX = swatchesStartX;
|
|
1614
|
-
swatchY =
|
|
1961
|
+
swatchY =
|
|
1962
|
+
colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1615
1963
|
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1616
1964
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1617
1965
|
// cursorY đã được cập nhật ở trên
|
|
@@ -1627,10 +1975,7 @@ const prepareExportCanvas = async (config, options = {}) => {
|
|
|
1627
1975
|
current: new Map(),
|
|
1628
1976
|
};
|
|
1629
1977
|
// Load fonts and images in parallel
|
|
1630
|
-
await Promise.all([
|
|
1631
|
-
preloadFonts(config),
|
|
1632
|
-
preloadImages(config, imageRefs)
|
|
1633
|
-
]);
|
|
1978
|
+
await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
|
|
1634
1979
|
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
1635
1980
|
if (!canvas.width || !canvas.height) {
|
|
1636
1981
|
return null;
|