embroidery-qc-image 1.0.29 → 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 +573 -141
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +573 -141
- 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
|
|
@@ -1162,25 +1501,127 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1162
1501
|
currentY += result.height;
|
|
1163
1502
|
}
|
|
1164
1503
|
if (showLabels.font && position.font) {
|
|
1165
|
-
// Render "Font: " với font mặc định
|
|
1166
1504
|
const prefix = "Font: ";
|
|
1167
|
-
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1168
|
-
const prefixWidth = ctx.measureText(prefix).width;
|
|
1169
|
-
let currentX = x + prefixWidth;
|
|
1170
|
-
ctx.fillText(prefix, x, currentY);
|
|
1171
|
-
// Render tên font với font từ config
|
|
1172
|
-
ctx.font = `${otherFontSize}px ${position.font}`;
|
|
1173
|
-
const fontNameWidth = ctx.measureText(position.font).width;
|
|
1174
|
-
ctx.fillText(position.font, currentX, currentY);
|
|
1175
|
-
currentX += fontNameWidth;
|
|
1176
|
-
// Render "(Mặc định)" hoặc "(Custom)" với font mặc định
|
|
1177
1505
|
const suffix = position.is_font_default === true ? " (Mặc định)" : " (Custom)";
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
ctx.fillText(suffix, currentX, currentY);
|
|
1181
|
-
// Tính toán height và di chuyển cursorY
|
|
1506
|
+
const fontName = position.font;
|
|
1507
|
+
const fullText = `${prefix}${fontName}${suffix}`;
|
|
1182
1508
|
const lineHeight = otherFontSize + lineGap;
|
|
1183
|
-
|
|
1509
|
+
const textTopY = currentY;
|
|
1510
|
+
const effectiveMaxWidth = mockupBounds
|
|
1511
|
+
? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
|
|
1512
|
+
: maxWidth;
|
|
1513
|
+
const MIN_FONT_FONT_SIZE = otherFontSize * 0.5;
|
|
1514
|
+
const measureFontTextWidth = (fontSize) => {
|
|
1515
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1516
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
1517
|
+
ctx.font = `${fontSize}px ${position.font}`;
|
|
1518
|
+
const fontNameWidth = ctx.measureText(fontName).width;
|
|
1519
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1520
|
+
const suffixWidth = ctx.measureText(suffix).width;
|
|
1521
|
+
return prefixWidth + fontNameWidth + suffixWidth;
|
|
1522
|
+
};
|
|
1523
|
+
const checkFontSizeFits = (fontSize) => {
|
|
1524
|
+
const textTopY = currentY;
|
|
1525
|
+
const lineHeight = fontSize + lineGap;
|
|
1526
|
+
const effectiveMaxWidth = mockupBounds
|
|
1527
|
+
? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
|
|
1528
|
+
: maxWidth;
|
|
1529
|
+
const textWidth = measureFontTextWidth(fontSize);
|
|
1530
|
+
return textWidth <= effectiveMaxWidth;
|
|
1531
|
+
};
|
|
1532
|
+
let effectiveFontSize = otherFontSize;
|
|
1533
|
+
let needsWrap = false;
|
|
1534
|
+
// Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
|
|
1535
|
+
const baseMaxWidth = measureFontTextWidth(otherFontSize);
|
|
1536
|
+
if (baseMaxWidth > effectiveMaxWidth) {
|
|
1537
|
+
// Binary search để tìm font size lớn nhất mà vẫn vừa
|
|
1538
|
+
let left = MIN_FONT_FONT_SIZE;
|
|
1539
|
+
let right = otherFontSize;
|
|
1540
|
+
let bestFontSize = MIN_FONT_FONT_SIZE;
|
|
1541
|
+
while (right - left > 0.1) {
|
|
1542
|
+
const mid = (left + right) / 2;
|
|
1543
|
+
if (checkFontSizeFits(mid)) {
|
|
1544
|
+
bestFontSize = mid;
|
|
1545
|
+
left = mid;
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
right = mid;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (checkFontSizeFits(bestFontSize)) {
|
|
1552
|
+
// Bước 1 thành công: font size đã shrink vừa chiều ngang
|
|
1553
|
+
effectiveFontSize = Math.floor(bestFontSize);
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
// Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
|
|
1557
|
+
needsWrap = true;
|
|
1558
|
+
effectiveFontSize = otherFontSize;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (needsWrap) {
|
|
1562
|
+
// Bước 2: Xuống dòng với font size gốc
|
|
1563
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1564
|
+
const wrappedLines = buildWrappedLines(ctx, fullText, effectiveMaxWidth, x, textTopY, lineHeight, mockupBounds);
|
|
1565
|
+
wrappedLines.forEach((line, index) => {
|
|
1566
|
+
const lineY = textTopY + index * lineHeight;
|
|
1567
|
+
const lineEffectiveMaxWidth = mockupBounds
|
|
1568
|
+
? getEffectiveMaxWidth(x, lineY, lineHeight, maxWidth, mockupBounds)
|
|
1569
|
+
: maxWidth;
|
|
1570
|
+
let lineToRender = line;
|
|
1571
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1572
|
+
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1573
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1574
|
+
lineToRender.length > 0) {
|
|
1575
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
ctx.fillText(lineToRender, x, lineY);
|
|
1579
|
+
});
|
|
1580
|
+
currentY += wrappedLines.length * lineHeight;
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
// Bước 1 thành công: Render với font size đã shrink (1 dòng)
|
|
1584
|
+
const shrunkTextTopY = currentY;
|
|
1585
|
+
const shrunkLineHeight = effectiveFontSize + lineGap;
|
|
1586
|
+
const shrunkEffectiveMaxWidth = mockupBounds
|
|
1587
|
+
? getEffectiveMaxWidth(x, shrunkTextTopY, shrunkLineHeight, maxWidth, mockupBounds)
|
|
1588
|
+
: maxWidth;
|
|
1589
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1590
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
1591
|
+
let currentX = x + prefixWidth;
|
|
1592
|
+
ctx.fillText(prefix, x, currentY);
|
|
1593
|
+
ctx.font = `${effectiveFontSize}px ${position.font}`;
|
|
1594
|
+
const fontNameWidth = ctx.measureText(fontName).width;
|
|
1595
|
+
const totalWidth = prefixWidth + fontNameWidth;
|
|
1596
|
+
if (totalWidth > shrunkEffectiveMaxWidth) {
|
|
1597
|
+
// Cần cắt font name
|
|
1598
|
+
let truncatedFontName = fontName;
|
|
1599
|
+
while (ctx.measureText(truncatedFontName).width >
|
|
1600
|
+
shrunkEffectiveMaxWidth - prefixWidth &&
|
|
1601
|
+
truncatedFontName.length > 0) {
|
|
1602
|
+
truncatedFontName = truncatedFontName.slice(0, -1);
|
|
1603
|
+
}
|
|
1604
|
+
ctx.fillText(truncatedFontName, currentX, currentY);
|
|
1605
|
+
currentX += ctx.measureText(truncatedFontName).width;
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
ctx.fillText(fontName, currentX, currentY);
|
|
1609
|
+
currentX += fontNameWidth;
|
|
1610
|
+
}
|
|
1611
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1612
|
+
const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
|
|
1613
|
+
if (remainingWidth > 0) {
|
|
1614
|
+
let truncatedSuffix = suffix;
|
|
1615
|
+
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1616
|
+
truncatedSuffix.length > 0) {
|
|
1617
|
+
truncatedSuffix = truncatedSuffix.slice(0, -1);
|
|
1618
|
+
}
|
|
1619
|
+
if (truncatedSuffix.length > 0) {
|
|
1620
|
+
ctx.fillText(truncatedSuffix, currentX, currentY);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
currentY += shrunkLineHeight;
|
|
1624
|
+
}
|
|
1184
1625
|
}
|
|
1185
1626
|
if (showLabels.color) {
|
|
1186
1627
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1213,7 +1654,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1213
1654
|
else {
|
|
1214
1655
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1215
1656
|
swatchX = swatchesStartX;
|
|
1216
|
-
swatchY =
|
|
1657
|
+
swatchY =
|
|
1658
|
+
result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1217
1659
|
currentY += result.height;
|
|
1218
1660
|
}
|
|
1219
1661
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -1322,130 +1764,122 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1322
1764
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1323
1765
|
const iconHeight = iconFontSize * 2;
|
|
1324
1766
|
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1325
|
-
iconImageReservedWidth =
|
|
1767
|
+
iconImageReservedWidth =
|
|
1768
|
+
iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1326
1769
|
}
|
|
1327
1770
|
}
|
|
1328
1771
|
}
|
|
1329
|
-
// Tính available width cho icon value (trừ đi khoảng trống cho icon_image)
|
|
1330
1772
|
const availableWidth = Math.max(1, maxWidth - labelWidth - iconImageReservedWidth);
|
|
1331
|
-
|
|
1332
|
-
|
|
1773
|
+
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1774
|
+
const valueStartX = x + labelWidth;
|
|
1775
|
+
const textTopY = textCenterY - iconFontSize / 2;
|
|
1776
|
+
const lineHeight = iconFontSize;
|
|
1777
|
+
const effectiveMaxWidth = mockupBounds
|
|
1778
|
+
? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
|
|
1779
|
+
: availableWidth;
|
|
1333
1780
|
const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
|
|
1334
1781
|
const measureIconValueWidth = (fontSize) => {
|
|
1335
1782
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1336
1783
|
return ctx.measureText(` ${iconValue}`).width;
|
|
1337
1784
|
};
|
|
1785
|
+
const checkFontSizeFits = (fontSize) => {
|
|
1786
|
+
const textTopY = textCenterY - fontSize / 2;
|
|
1787
|
+
const lineHeight = fontSize;
|
|
1788
|
+
const effectiveMaxWidth = mockupBounds
|
|
1789
|
+
? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
|
|
1790
|
+
: availableWidth;
|
|
1791
|
+
const textWidth = measureIconValueWidth(fontSize);
|
|
1792
|
+
return textWidth <= effectiveMaxWidth;
|
|
1793
|
+
};
|
|
1338
1794
|
let effectiveIconValueFontSize = iconFontSize;
|
|
1339
|
-
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1340
1795
|
let needsWrap = false;
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1796
|
+
// Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
|
|
1797
|
+
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1798
|
+
if (baseMaxWidth > effectiveMaxWidth) {
|
|
1799
|
+
// Binary search để tìm font size lớn nhất mà vẫn vừa
|
|
1800
|
+
let left = MIN_ICON_VALUE_FONT_SIZE;
|
|
1801
|
+
let right = iconFontSize;
|
|
1802
|
+
let bestFontSize = MIN_ICON_VALUE_FONT_SIZE;
|
|
1803
|
+
while (right - left > 0.1) {
|
|
1804
|
+
const mid = (left + right) / 2;
|
|
1805
|
+
if (checkFontSizeFits(mid)) {
|
|
1806
|
+
bestFontSize = mid;
|
|
1807
|
+
left = mid;
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
right = mid;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (checkFontSizeFits(bestFontSize)) {
|
|
1814
|
+
// Bước 1 thành công: font size đã shrink vừa chiều ngang
|
|
1815
|
+
effectiveIconValueFontSize = Math.floor(bestFontSize);
|
|
1816
|
+
}
|
|
1817
|
+
else {
|
|
1818
|
+
// Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
|
|
1348
1819
|
needsWrap = true;
|
|
1349
|
-
effectiveIconValueFontSize =
|
|
1820
|
+
effectiveIconValueFontSize = iconFontSize;
|
|
1350
1821
|
}
|
|
1351
1822
|
}
|
|
1352
|
-
//
|
|
1353
|
-
const valueLineHeight = effectiveIconValueFontSize;
|
|
1354
|
-
let allWrappedLines = [];
|
|
1355
|
-
// Text align center: căn giữa theo chiều dọc trong block
|
|
1356
|
-
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1357
|
-
const valueStartX = x + labelWidth;
|
|
1358
|
-
if (needsWrap) {
|
|
1359
|
-
// Dùng wrap text logic
|
|
1360
|
-
const wrappedLines = buildWrappedLines(ctx, iconValue, availableWidth, valueStartX, textCenterY, valueLineHeight, mockupBounds);
|
|
1361
|
-
allWrappedLines = wrappedLines;
|
|
1362
|
-
wrappedLines.length * valueLineHeight;
|
|
1363
|
-
}
|
|
1364
|
-
else {
|
|
1365
|
-
// Không cần wrap, chỉ một dòng
|
|
1366
|
-
allWrappedLines = [iconValue];
|
|
1367
|
-
}
|
|
1368
|
-
// Vẽ label với textBaseline = middle để align center với value
|
|
1823
|
+
// Bước 2: Nếu bước 1 thất bại, xuống dòng với font size gốc
|
|
1369
1824
|
ctx.textBaseline = "middle";
|
|
1370
1825
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1371
1826
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1372
1827
|
ctx.fillText(iconLabel, x, textCenterY);
|
|
1373
|
-
// Vẽ icon value với align center
|
|
1374
1828
|
ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1375
1829
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1376
1830
|
let maxValueLineWidth = 0;
|
|
1377
|
-
|
|
1831
|
+
let totalTextHeight = iconImageHeight;
|
|
1378
1832
|
if (needsWrap) {
|
|
1379
|
-
//
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
maxValueLineWidth = w;
|
|
1396
|
-
}
|
|
1397
|
-
else {
|
|
1398
|
-
ctx.fillText(line, valueStartX, lineY);
|
|
1399
|
-
const w = ctx.measureText(line).width;
|
|
1400
|
-
if (w > maxValueLineWidth)
|
|
1401
|
-
maxValueLineWidth = w;
|
|
1833
|
+
// Bước 2: Xuống dòng với font size gốc
|
|
1834
|
+
const wrappedLines = buildWrappedLines(ctx, iconValue, effectiveMaxWidth, valueStartX, textTopY, lineHeight, mockupBounds);
|
|
1835
|
+
// Tính height dựa trên số dòng thực tế
|
|
1836
|
+
totalTextHeight = Math.max(iconImageHeight, wrappedLines.length * lineHeight);
|
|
1837
|
+
wrappedLines.forEach((line, index) => {
|
|
1838
|
+
const lineTopY = textTopY + index * lineHeight;
|
|
1839
|
+
const lineCenterY = lineTopY + lineHeight / 2;
|
|
1840
|
+
const lineEffectiveMaxWidth = mockupBounds
|
|
1841
|
+
? getEffectiveMaxWidth(valueStartX, lineTopY, lineHeight, availableWidth, mockupBounds)
|
|
1842
|
+
: availableWidth;
|
|
1843
|
+
let lineToRender = line;
|
|
1844
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1845
|
+
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1846
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1847
|
+
lineToRender.length > 0) {
|
|
1848
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1402
1849
|
}
|
|
1403
1850
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
maxValueLineWidth = w;
|
|
1409
|
-
}
|
|
1851
|
+
ctx.fillText(lineToRender, valueStartX, lineCenterY);
|
|
1852
|
+
const w = ctx.measureText(lineToRender).width;
|
|
1853
|
+
if (w > maxValueLineWidth)
|
|
1854
|
+
maxValueLineWidth = w;
|
|
1410
1855
|
});
|
|
1411
1856
|
}
|
|
1412
1857
|
else {
|
|
1413
|
-
//
|
|
1858
|
+
// Bước 1 thành công: Render với font size đã shrink (1 dòng)
|
|
1859
|
+
const shrunkTextTopY = textCenterY - effectiveIconValueFontSize / 2;
|
|
1860
|
+
const shrunkLineHeight = effectiveIconValueFontSize;
|
|
1861
|
+
const shrunkEffectiveMaxWidth = mockupBounds
|
|
1862
|
+
? getEffectiveMaxWidth(valueStartX, shrunkTextTopY, shrunkLineHeight, availableWidth, mockupBounds)
|
|
1863
|
+
: availableWidth;
|
|
1414
1864
|
const lineText = ` ${iconValue}`;
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
let truncatedLine = lineText;
|
|
1422
|
-
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1423
|
-
truncatedLine = truncatedLine.slice(0, -1);
|
|
1424
|
-
}
|
|
1425
|
-
ctx.fillText(truncatedLine, valueStartX, textCenterY);
|
|
1426
|
-
const w = ctx.measureText(truncatedLine).width;
|
|
1427
|
-
if (w > maxValueLineWidth)
|
|
1428
|
-
maxValueLineWidth = w;
|
|
1429
|
-
}
|
|
1430
|
-
else {
|
|
1431
|
-
ctx.fillText(lineText, valueStartX, textCenterY);
|
|
1432
|
-
const w = ctx.measureText(lineText).width;
|
|
1433
|
-
if (w > maxValueLineWidth)
|
|
1434
|
-
maxValueLineWidth = w;
|
|
1865
|
+
let lineToRender = lineText;
|
|
1866
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1867
|
+
if (lineWidth > shrunkEffectiveMaxWidth) {
|
|
1868
|
+
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1869
|
+
lineToRender.length > 0) {
|
|
1870
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1435
1871
|
}
|
|
1436
1872
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
maxValueLineWidth = w;
|
|
1442
|
-
}
|
|
1873
|
+
ctx.fillText(lineToRender, valueStartX, textCenterY);
|
|
1874
|
+
const w = ctx.measureText(lineToRender).width;
|
|
1875
|
+
if (w > maxValueLineWidth)
|
|
1876
|
+
maxValueLineWidth = w;
|
|
1443
1877
|
}
|
|
1444
1878
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1445
1879
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1446
1880
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1447
1881
|
const iconResult = {
|
|
1448
|
-
height:
|
|
1882
|
+
height: totalTextHeight + lineGap,
|
|
1449
1883
|
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1450
1884
|
lastLineWidth: labelWidth + maxValueLineWidth};
|
|
1451
1885
|
// Kiểm tra xem phần icon image có vượt quá canvas không trước khi render
|
|
@@ -1495,7 +1929,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1495
1929
|
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1496
1930
|
testLines.length * lineHeight;
|
|
1497
1931
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1498
|
-
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1932
|
+
const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
|
|
1499
1933
|
const textEndX = x + testTextWidth;
|
|
1500
1934
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1501
1935
|
const swatchesStartX = textEndX + spacing;
|
|
@@ -1524,7 +1958,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1524
1958
|
else {
|
|
1525
1959
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1526
1960
|
swatchX = swatchesStartX;
|
|
1527
|
-
swatchY =
|
|
1961
|
+
swatchY =
|
|
1962
|
+
colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1528
1963
|
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1529
1964
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1530
1965
|
// cursorY đã được cập nhật ở trên
|
|
@@ -1540,10 +1975,7 @@ const prepareExportCanvas = async (config, options = {}) => {
|
|
|
1540
1975
|
current: new Map(),
|
|
1541
1976
|
};
|
|
1542
1977
|
// Load fonts and images in parallel
|
|
1543
|
-
await Promise.all([
|
|
1544
|
-
preloadFonts(config),
|
|
1545
|
-
preloadImages(config, imageRefs)
|
|
1546
|
-
]);
|
|
1978
|
+
await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
|
|
1547
1979
|
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
1548
1980
|
if (!canvas.width || !canvas.height) {
|
|
1549
1981
|
return null;
|