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.esm.js
CHANGED
|
@@ -31,6 +31,71 @@ function styleInject(css, ref) {
|
|
|
31
31
|
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";
|
|
32
32
|
styleInject(css_248z);
|
|
33
33
|
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// CONSTANTS
|
|
36
|
+
// ============================================================================
|
|
37
|
+
const COLOR_MAP = {
|
|
38
|
+
"Army (1394)": "#545541",
|
|
39
|
+
Army: "#545541",
|
|
40
|
+
"Black (8)": "#060608",
|
|
41
|
+
Black: "#060608",
|
|
42
|
+
"Bubblegum (1309)": "#E77B9F",
|
|
43
|
+
Bubblegum: "#E77B9F",
|
|
44
|
+
"Carolina Blue (1274)": "#608CC9",
|
|
45
|
+
"Carolina Blue": "#608CC9",
|
|
46
|
+
"Celadon (1098)": "#8EAD8D",
|
|
47
|
+
Celadon: "#8EAD8D",
|
|
48
|
+
"Coffee Bean (1145)": "#502B23",
|
|
49
|
+
"Coffee Bean": "#502B23",
|
|
50
|
+
"Daffodil (1180)": "#FBE30D",
|
|
51
|
+
Daffodil: "#FBE30D",
|
|
52
|
+
"Dark Gray (1131)": "#2E272E",
|
|
53
|
+
"Dark Gray": "#2E272E",
|
|
54
|
+
"Doe Skin Beige (1344)": "#AE9B8B",
|
|
55
|
+
"Doe Skin Beige": "#AE9B8B",
|
|
56
|
+
"Dusty Blue (1373)": "#7B90A9",
|
|
57
|
+
"Dusty Blue": "#7B90A9",
|
|
58
|
+
"Forest Green (1397)": "#073020",
|
|
59
|
+
"Forest Green": "#073020",
|
|
60
|
+
"Gold (1425)": "#D2920A",
|
|
61
|
+
Gold: "#D2920A",
|
|
62
|
+
"Gray (1118)": "#9999A3",
|
|
63
|
+
Gray: "#9999A3",
|
|
64
|
+
"Ivory (1072)": "#E3DAC9",
|
|
65
|
+
Ivory: "#E3DAC9",
|
|
66
|
+
"Lavender (1032)": "#9274B6",
|
|
67
|
+
Lavender: "#9274B6",
|
|
68
|
+
"Light Denim (1133)": "#366696",
|
|
69
|
+
"Light Denim": "#366696",
|
|
70
|
+
"Light Salmon (1018)": "#E0A793",
|
|
71
|
+
"Light Salmon": "#E0A793",
|
|
72
|
+
"Maroon (1374)": "#480C1C",
|
|
73
|
+
Maroon: "#480C1C",
|
|
74
|
+
"Navy Blue (1044)": "#04072A",
|
|
75
|
+
"Navy Blue": "#04072A",
|
|
76
|
+
"Olive Green (1157)": "#625E1F",
|
|
77
|
+
"Olive Green": "#625E1F",
|
|
78
|
+
"Orange (1278)": "#D45D03",
|
|
79
|
+
Orange: "#D45D03",
|
|
80
|
+
"Peach Blush (1053)": "#E2C0B6",
|
|
81
|
+
"Peach Blush": "#E2C0B6",
|
|
82
|
+
"Pink (1148)": "#EFAFBF",
|
|
83
|
+
Pink: "#EFAFBF",
|
|
84
|
+
"Purple (1412)": "#37196F",
|
|
85
|
+
Purple: "#37196F",
|
|
86
|
+
"Red (1037)": "#9D000B",
|
|
87
|
+
Red: "#9D000B",
|
|
88
|
+
"Silver Sage (1396)": "#424F45",
|
|
89
|
+
"Silver Sage": "#424F45",
|
|
90
|
+
"Summer Sky (1432)": "#65A8D2",
|
|
91
|
+
"Summer Sky": "#65A8D2",
|
|
92
|
+
"Terra Cotta (1477)": "#AE3111",
|
|
93
|
+
"Terra Cotta": "#AE3111",
|
|
94
|
+
"Sand (1055)": "#D2C2AB",
|
|
95
|
+
Sand: "#D2C2AB",
|
|
96
|
+
"White (9)": "#D8D7DC",
|
|
97
|
+
White: "#D8D7DC",
|
|
98
|
+
};
|
|
34
99
|
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
35
100
|
const DEFAULT_WARNING_COLOR = "#FF8C00";
|
|
36
101
|
const BASE_URLS = {
|
|
@@ -71,7 +136,7 @@ const loadFont = (fontName) => {
|
|
|
71
136
|
// Check if font is already loaded in document.fonts (browser cache)
|
|
72
137
|
const fontFaceSet = document.fonts;
|
|
73
138
|
for (const font of fontFaceSet) {
|
|
74
|
-
if (font.family === fontName && font.status ===
|
|
139
|
+
if (font.family === fontName && font.status === "loaded") {
|
|
75
140
|
return Promise.resolve();
|
|
76
141
|
}
|
|
77
142
|
}
|
|
@@ -191,7 +256,9 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
191
256
|
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
192
257
|
finalize();
|
|
193
258
|
};
|
|
194
|
-
const desiredSrc = attemptedProxy
|
|
259
|
+
const desiredSrc = attemptedProxy
|
|
260
|
+
? getProxyUrl(getResizeUrl(url))
|
|
261
|
+
: getResizeUrl(url);
|
|
195
262
|
if (target.src !== desiredSrc) {
|
|
196
263
|
target.src = desiredSrc;
|
|
197
264
|
}
|
|
@@ -204,22 +271,22 @@ const getResizeUrl = (url) => {
|
|
|
204
271
|
try {
|
|
205
272
|
const urlObj = new URL(url);
|
|
206
273
|
// Xử lý cdn.shopify.com
|
|
207
|
-
if (urlObj.hostname ===
|
|
274
|
+
if (urlObj.hostname === "cdn.shopify.com") {
|
|
208
275
|
// Set hoặc update query param width=400
|
|
209
|
-
urlObj.searchParams.set(
|
|
276
|
+
urlObj.searchParams.set("width", "400");
|
|
210
277
|
return urlObj.toString();
|
|
211
278
|
}
|
|
212
279
|
// Xử lý m.media-amazon.com
|
|
213
|
-
if (urlObj.hostname ===
|
|
280
|
+
if (urlObj.hostname === "m.media-amazon.com") {
|
|
214
281
|
const pathname = urlObj.pathname;
|
|
215
282
|
// Split pathname theo dấu /
|
|
216
|
-
const pathArr = pathname.split(
|
|
283
|
+
const pathArr = pathname.split("/");
|
|
217
284
|
// Lấy filename (phần cuối cùng)
|
|
218
285
|
const filename = pathArr[pathArr.length - 1];
|
|
219
286
|
// Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
|
|
220
|
-
const cleanedFilename = filename.replace(/\._.*_/g,
|
|
287
|
+
const cleanedFilename = filename.replace(/\._.*_/g, "");
|
|
221
288
|
// Split filename đã clean theo dấu .
|
|
222
|
-
const parts = cleanedFilename.split(
|
|
289
|
+
const parts = cleanedFilename.split(".");
|
|
223
290
|
if (parts.length >= 2) {
|
|
224
291
|
// Lấy phần đầu và phần cuối
|
|
225
292
|
const firstPart = parts[0];
|
|
@@ -229,16 +296,16 @@ const getResizeUrl = (url) => {
|
|
|
229
296
|
// Thay filename mới vào pathArr
|
|
230
297
|
pathArr[pathArr.length - 1] = newFilename;
|
|
231
298
|
// Join lại
|
|
232
|
-
urlObj.pathname = pathArr.join(
|
|
299
|
+
urlObj.pathname = pathArr.join("/");
|
|
233
300
|
return urlObj.toString();
|
|
234
301
|
}
|
|
235
302
|
}
|
|
236
303
|
// Xử lý i.etsystatic.com
|
|
237
|
-
if (urlObj.hostname ===
|
|
304
|
+
if (urlObj.hostname === "i.etsystatic.com") {
|
|
238
305
|
const pathname = urlObj.pathname;
|
|
239
306
|
// Thay il_fullxfull bằng il_400x400
|
|
240
|
-
if (pathname.includes(
|
|
241
|
-
const newPathname = pathname.replace(/il_fullxfull/g,
|
|
307
|
+
if (pathname.includes("il_fullxfull")) {
|
|
308
|
+
const newPathname = pathname.replace(/il_fullxfull/g, "il_400x400");
|
|
242
309
|
urlObj.pathname = newPathname;
|
|
243
310
|
return urlObj.toString();
|
|
244
311
|
}
|
|
@@ -362,7 +429,8 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight, mockupBounds = null) =>
|
|
|
362
429
|
let lineToRender = line;
|
|
363
430
|
if (ctx.measureText(line).width > effectiveMaxWidth) {
|
|
364
431
|
// Cắt từng ký tự cho đến khi vừa
|
|
365
|
-
while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
|
|
432
|
+
while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
|
|
433
|
+
lineToRender.length > 0) {
|
|
366
434
|
lineToRender = lineToRender.slice(0, -1);
|
|
367
435
|
}
|
|
368
436
|
}
|
|
@@ -533,6 +601,262 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
533
601
|
// ============================================================================
|
|
534
602
|
// RENDERING FUNCTIONS
|
|
535
603
|
// ============================================================================
|
|
604
|
+
const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
|
|
605
|
+
// Clear canvas
|
|
606
|
+
ctx.fillStyle = "#e7e7e7";
|
|
607
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
608
|
+
const padding = LAYOUT.PADDING * 6;
|
|
609
|
+
const usableWidth = canvas.width - padding * 2;
|
|
610
|
+
const usableHeight = canvas.height - padding * 2;
|
|
611
|
+
// Calculate sections
|
|
612
|
+
const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
|
|
613
|
+
const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
|
|
614
|
+
const topSectionY = padding;
|
|
615
|
+
const bottomSectionY = topSectionY + topSectionHeight;
|
|
616
|
+
// Get first side (stroke_patches should have only one side)
|
|
617
|
+
const side = config.sides[0];
|
|
618
|
+
if (!side || !side.positions.length) {
|
|
619
|
+
renderErrorState(ctx, canvas, "Không có dữ liệu positions");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const position = side.positions[0];
|
|
623
|
+
if (position.type !== "TEXT") {
|
|
624
|
+
renderErrorState(ctx, canvas, "Position phải là TEXT");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Validate required fields
|
|
628
|
+
if (!position.font) {
|
|
629
|
+
renderErrorState(ctx, canvas, "Thiếu font");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
// Validate layer_colors
|
|
633
|
+
if (!position.layer_colors) {
|
|
634
|
+
renderErrorState(ctx, canvas, "Thiếu layer_colors");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (position.layer_colors.length !== 3) {
|
|
638
|
+
renderErrorState(ctx, canvas, `layer_colors phải có đúng 3 phần tử, hiện tại có ${position.layer_colors.length}`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Validate từng màu
|
|
642
|
+
const [textColor, borderColor, backgroundColor] = position.layer_colors;
|
|
643
|
+
const missingColors = [];
|
|
644
|
+
if (!textColor || textColor.trim() === "") {
|
|
645
|
+
missingColors.push("Màu text (vị trí 1)");
|
|
646
|
+
}
|
|
647
|
+
if (!borderColor || borderColor.trim() === "") {
|
|
648
|
+
missingColors.push("Màu border (vị trí 2)");
|
|
649
|
+
}
|
|
650
|
+
if (!backgroundColor || backgroundColor.trim() === "") {
|
|
651
|
+
missingColors.push("Màu background (vị trí 3)");
|
|
652
|
+
}
|
|
653
|
+
if (missingColors.length > 0) {
|
|
654
|
+
renderErrorState(ctx, canvas, `Thiếu màu trong layer_colors:\n${missingColors.join("\n")}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// TOP SECTION (2/3): Hiển thị mẫu preview
|
|
659
|
+
// ============================================================================
|
|
660
|
+
ctx.save();
|
|
661
|
+
// Draw "Hình mẫu:" label at the top
|
|
662
|
+
const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
|
|
663
|
+
ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
664
|
+
ctx.fillStyle = "#CC0000"; // Red color
|
|
665
|
+
ctx.fillText("Hình mẫu:", padding, topSectionY);
|
|
666
|
+
// Adjust top section Y to account for title + extra spacing (40px)
|
|
667
|
+
const extraSpacing = 40;
|
|
668
|
+
const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
|
|
669
|
+
const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
|
|
670
|
+
// Calculate text size to fit top section
|
|
671
|
+
let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
|
|
672
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
673
|
+
const text = position.text || "";
|
|
674
|
+
const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
|
|
675
|
+
const maxTextHeight = actualTopSectionHeight; // Use 60% of height
|
|
676
|
+
// Scale down font size to fit
|
|
677
|
+
let textWidth = ctx.measureText(text).width;
|
|
678
|
+
while (textWidth > maxTextWidth && previewFontSize > 50) {
|
|
679
|
+
previewFontSize *= 0.95;
|
|
680
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
681
|
+
textWidth = ctx.measureText(text).width;
|
|
682
|
+
}
|
|
683
|
+
// Ensure text height also fits
|
|
684
|
+
while (previewFontSize > maxTextHeight && previewFontSize > 50) {
|
|
685
|
+
previewFontSize *= 0.95;
|
|
686
|
+
ctx.font = `${previewFontSize}px ${position.font}`;
|
|
687
|
+
}
|
|
688
|
+
// Update textWidth after final scaling
|
|
689
|
+
textWidth = ctx.measureText(text).width;
|
|
690
|
+
// Center the text in top section
|
|
691
|
+
const textX = padding + usableWidth / 2 - textWidth / 2;
|
|
692
|
+
const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
|
|
693
|
+
// Get color hex values
|
|
694
|
+
const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
|
|
695
|
+
const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
|
|
696
|
+
const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
|
|
697
|
+
// Calculate stroke widths
|
|
698
|
+
// Background needs to be MUCH thicker to create spacing from text
|
|
699
|
+
// Border needs to be even thicker to wrap around background
|
|
700
|
+
const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
|
|
701
|
+
const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
|
|
702
|
+
// Layer 1: Draw border stroke (outermost)
|
|
703
|
+
ctx.strokeStyle = borderColorHex;
|
|
704
|
+
ctx.lineWidth = borderWidth;
|
|
705
|
+
ctx.lineJoin = "round";
|
|
706
|
+
ctx.lineCap = "round";
|
|
707
|
+
ctx.strokeText(text, textX, textY);
|
|
708
|
+
// Layer 2: Draw background color stroke (middle - creates spacing from text)
|
|
709
|
+
ctx.strokeStyle = bgColorHex;
|
|
710
|
+
ctx.lineWidth = bgWidth;
|
|
711
|
+
ctx.lineJoin = "round";
|
|
712
|
+
ctx.lineCap = "round";
|
|
713
|
+
ctx.strokeText(text, textX, textY);
|
|
714
|
+
// Layer 3: Draw text fill ONLY (no stroke on text itself)
|
|
715
|
+
ctx.fillStyle = textColorHex;
|
|
716
|
+
ctx.fillText(text, textX, textY);
|
|
717
|
+
ctx.restore();
|
|
718
|
+
// ============================================================================
|
|
719
|
+
// BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
|
|
720
|
+
// ============================================================================
|
|
721
|
+
ctx.save();
|
|
722
|
+
// Draw border around bottom section for debugging
|
|
723
|
+
// ctx.strokeStyle = "#ccc";
|
|
724
|
+
// ctx.lineWidth = 2;
|
|
725
|
+
// ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
|
|
726
|
+
const bottomPadding = 0; // Không padding ngang, sát lề
|
|
727
|
+
const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
|
|
728
|
+
const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
|
|
729
|
+
// Split bottom section: 60% left for info, 40% right for image
|
|
730
|
+
const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
|
|
731
|
+
const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
|
|
732
|
+
const imageSectionX = padding + infoSectionWidth;
|
|
733
|
+
// Left side: Info list
|
|
734
|
+
const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
|
|
735
|
+
const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
|
|
736
|
+
let infoY = bottomSectionY + LAYOUT.PADDING;
|
|
737
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
738
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
739
|
+
ctx.textAlign = "left";
|
|
740
|
+
ctx.textBaseline = "top";
|
|
741
|
+
// Asterisk prefix style
|
|
742
|
+
const drawAsterisk = (x, y) => {
|
|
743
|
+
ctx.save();
|
|
744
|
+
ctx.fillStyle = "#CC0000"; // Red asterisk
|
|
745
|
+
ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
746
|
+
ctx.fillText("*", x, y);
|
|
747
|
+
ctx.restore();
|
|
748
|
+
};
|
|
749
|
+
const asteriskWidth = ctx.measureText("*").width + 5;
|
|
750
|
+
const startX = padding + asteriskWidth;
|
|
751
|
+
// Font - render "Font: " với font mặc định, tên font với font đó
|
|
752
|
+
drawAsterisk(padding, infoY);
|
|
753
|
+
const fontPrefix = "Font: ";
|
|
754
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
755
|
+
ctx.fillText(fontPrefix, startX, infoY);
|
|
756
|
+
// Render font name với chính font đó
|
|
757
|
+
const prefixWidth = ctx.measureText(fontPrefix).width;
|
|
758
|
+
ctx.font = `${infoFontSize}px ${position.font}`;
|
|
759
|
+
ctx.fillText(position.font, startX + prefixWidth, infoY);
|
|
760
|
+
infoY += infoLineHeight;
|
|
761
|
+
// Reset font về mặc định cho các dòng tiếp theo
|
|
762
|
+
ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
763
|
+
// Màu chữ (Text Color)
|
|
764
|
+
drawAsterisk(padding, infoY);
|
|
765
|
+
const textColorLabel = `Màu chữ: ${textColor}`;
|
|
766
|
+
ctx.fillText(textColorLabel, startX, infoY);
|
|
767
|
+
// Draw text color swatch
|
|
768
|
+
const swatchSize = infoFontSize * 1.3; // Giảm từ 1.5 xuống 1.3
|
|
769
|
+
const swatchX = startX +
|
|
770
|
+
ctx.measureText(textColorLabel).width +
|
|
771
|
+
LAYOUT.ELEMENT_SPACING * 0.3; // Giảm spacing
|
|
772
|
+
const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
773
|
+
const textColorSwatchUrl = getImageUrl("threadColor", textColor);
|
|
774
|
+
const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
|
|
775
|
+
if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
|
|
776
|
+
const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
|
|
777
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
778
|
+
ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
|
|
779
|
+
}
|
|
780
|
+
infoY += infoLineHeight;
|
|
781
|
+
// Màu nền (Background Color)
|
|
782
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
783
|
+
const bgColorLabel = `Màu nền: ${backgroundColor}`;
|
|
784
|
+
ctx.fillText(bgColorLabel, startX, infoY);
|
|
785
|
+
// Draw background color swatch
|
|
786
|
+
const bgSwatchX = startX + ctx.measureText(bgColorLabel).width + LAYOUT.ELEMENT_SPACING * 0.3;
|
|
787
|
+
const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
788
|
+
const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
|
|
789
|
+
const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
|
|
790
|
+
if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
|
|
791
|
+
const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
|
|
792
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
793
|
+
ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
|
|
794
|
+
}
|
|
795
|
+
infoY += infoLineHeight;
|
|
796
|
+
// Màu viền (Border Color)
|
|
797
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
798
|
+
const borderColorLabel = `Màu viền: ${borderColor}`;
|
|
799
|
+
ctx.fillText(borderColorLabel, startX, infoY);
|
|
800
|
+
// Draw border color swatch
|
|
801
|
+
const borderSwatchX = startX +
|
|
802
|
+
ctx.measureText(borderColorLabel).width +
|
|
803
|
+
LAYOUT.ELEMENT_SPACING * 0.3;
|
|
804
|
+
const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
|
|
805
|
+
const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
|
|
806
|
+
const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
|
|
807
|
+
if (borderColorSwatchImg?.complete &&
|
|
808
|
+
borderColorSwatchImg.naturalHeight > 0) {
|
|
809
|
+
const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
|
|
810
|
+
const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
|
|
811
|
+
ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
|
|
812
|
+
}
|
|
813
|
+
infoY += infoLineHeight;
|
|
814
|
+
// Attachment
|
|
815
|
+
if (position.attachment) {
|
|
816
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
817
|
+
const attachmentLabel = `Attachment: ${position.attachment}`;
|
|
818
|
+
ctx.fillText(attachmentLabel, startX, infoY);
|
|
819
|
+
infoY += infoLineHeight;
|
|
820
|
+
}
|
|
821
|
+
// Size
|
|
822
|
+
if (side.size) {
|
|
823
|
+
drawAsterisk(padding + bottomPadding, infoY);
|
|
824
|
+
const sizeLabel = `Size: ${side.size}`;
|
|
825
|
+
ctx.fillText(sizeLabel, startX, infoY);
|
|
826
|
+
infoY += infoLineHeight;
|
|
827
|
+
}
|
|
828
|
+
// Right side: Image from config.image_url
|
|
829
|
+
if (config.image_url) {
|
|
830
|
+
// Draw "Mockup" label
|
|
831
|
+
ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
|
|
832
|
+
ctx.fillStyle = "#000000";
|
|
833
|
+
const mockupLabel = "Mockup";
|
|
834
|
+
const mockupLabelWidth = ctx.measureText(mockupLabel).width;
|
|
835
|
+
const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
|
|
836
|
+
ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
|
|
837
|
+
const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
|
|
838
|
+
const img = imageRefs.current.get(config.image_url) ??
|
|
839
|
+
imageRefs.current.get("mockup");
|
|
840
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
841
|
+
const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
|
|
842
|
+
const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
|
|
843
|
+
const imgAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
844
|
+
let drawWidth = maxImgWidth;
|
|
845
|
+
let drawHeight = drawWidth / imgAspectRatio;
|
|
846
|
+
if (drawHeight > maxImgHeight) {
|
|
847
|
+
drawHeight = maxImgHeight;
|
|
848
|
+
drawWidth = drawHeight * imgAspectRatio;
|
|
849
|
+
}
|
|
850
|
+
const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
|
|
851
|
+
const imgY = bottomSectionY +
|
|
852
|
+
LAYOUT.PADDING +
|
|
853
|
+
mockupLabelHeight +
|
|
854
|
+
(bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
|
|
855
|
+
ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
ctx.restore();
|
|
859
|
+
};
|
|
536
860
|
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
537
861
|
const ctx = canvas.getContext("2d");
|
|
538
862
|
if (!ctx)
|
|
@@ -547,6 +871,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
547
871
|
}
|
|
548
872
|
if (!config.sides?.length)
|
|
549
873
|
return;
|
|
874
|
+
// Check if this is a stroke_patches layout
|
|
875
|
+
const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
|
|
876
|
+
if (hasStrokePatches) {
|
|
877
|
+
renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
550
880
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
551
881
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
552
882
|
if (config.image_url) {
|
|
@@ -719,7 +1049,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
|
|
|
719
1049
|
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
720
1050
|
const lineTopY = y;
|
|
721
1051
|
const lineBottomY = y + lineHeight;
|
|
722
|
-
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1052
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
|
|
1053
|
+
lineBottomY > mockupBounds.y;
|
|
723
1054
|
if (overlapsY) {
|
|
724
1055
|
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
725
1056
|
const maxAllowedWidth = mockupBounds.x - x;
|
|
@@ -766,15 +1097,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
766
1097
|
const iconPositions = side.positions.filter((p) => p.type === "ICON");
|
|
767
1098
|
// Kiểm tra tất cả TEXT positions có trống không
|
|
768
1099
|
// Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
|
|
769
|
-
const allTextEmpty = textPositions.length === 0 ||
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1100
|
+
const allTextEmpty = textPositions.length === 0 ||
|
|
1101
|
+
textPositions.every((p) => {
|
|
1102
|
+
const text = p.text ?? "";
|
|
1103
|
+
return text.trim() === "";
|
|
1104
|
+
});
|
|
773
1105
|
// Kiểm tra tất cả ICON positions có is_delete_icon = true không
|
|
774
1106
|
// Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
|
|
775
|
-
const allIconsDeleted = iconPositions.length === 0 ||
|
|
776
|
-
|
|
777
|
-
|
|
1107
|
+
const allIconsDeleted = iconPositions.length === 0 ||
|
|
1108
|
+
iconPositions.every((p) => {
|
|
1109
|
+
return p.is_delete_icon === true;
|
|
1110
|
+
});
|
|
778
1111
|
// 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ì)"
|
|
779
1112
|
if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
|
|
780
1113
|
ctx.save();
|
|
@@ -788,7 +1121,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
|
|
|
788
1121
|
return currentY - startY;
|
|
789
1122
|
}
|
|
790
1123
|
// Compute uniform properties
|
|
791
|
-
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1124
|
+
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
|
|
1125
|
+
(!p.layer_colors?.length || p.layer_colors.length === 1));
|
|
792
1126
|
const iconColorValues = iconColorPositions
|
|
793
1127
|
.map((p) => {
|
|
794
1128
|
if (p.layer_colors?.length === 1)
|
|
@@ -910,7 +1244,8 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
910
1244
|
...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
|
|
911
1245
|
];
|
|
912
1246
|
if (textPositions.length === 0 &&
|
|
913
|
-
(!options?.additionalColorValues ||
|
|
1247
|
+
(!options?.additionalColorValues ||
|
|
1248
|
+
options.additionalColorValues.length === 0)) {
|
|
914
1249
|
return defaults;
|
|
915
1250
|
}
|
|
916
1251
|
const colors = new Set(colorSources);
|
|
@@ -1011,7 +1346,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1011
1346
|
}
|
|
1012
1347
|
rendered++;
|
|
1013
1348
|
}
|
|
1014
|
-
if (values.floral &&
|
|
1349
|
+
if (values.floral &&
|
|
1350
|
+
values.floral !== "None" &&
|
|
1351
|
+
shouldRenderField("floral")) {
|
|
1015
1352
|
const floralUrl = getImageUrl("floral", values.floral);
|
|
1016
1353
|
const floralImg = imageRefs.current.get(floralUrl);
|
|
1017
1354
|
// Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
|
|
@@ -1075,9 +1412,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1075
1412
|
let currentY = y;
|
|
1076
1413
|
// Chuẩn hóa xuống dòng:
|
|
1077
1414
|
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1078
|
-
const normalizeNewlines = (text) => text
|
|
1079
|
-
.replace(/\r\n/g, "\n")
|
|
1080
|
-
.replace(/\r/g, "\n");
|
|
1415
|
+
const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1081
1416
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1082
1417
|
const rawOriginalText = position.text ?? "";
|
|
1083
1418
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1114,7 +1449,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1114
1449
|
let minShrinkRatio = 1;
|
|
1115
1450
|
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1116
1451
|
lines.forEach((line, idx) => {
|
|
1117
|
-
const lineY = textCenterY -
|
|
1452
|
+
const lineY = textCenterY -
|
|
1453
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1454
|
+
idx * valueLineHeight;
|
|
1118
1455
|
// Tính effectiveMaxWidth cho dòng này
|
|
1119
1456
|
const effectiveMaxWidth = mockupBounds
|
|
1120
1457
|
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
@@ -1142,7 +1479,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1142
1479
|
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1143
1480
|
// 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
|
|
1144
1481
|
lines.forEach((line, idx) => {
|
|
1145
|
-
const lineY = textCenterY -
|
|
1482
|
+
const lineY = textCenterY -
|
|
1483
|
+
((lines.length - 1) / 2) * valueLineHeight +
|
|
1484
|
+
idx * valueLineHeight;
|
|
1146
1485
|
ctx.fillText(line, valueStartX, lineY);
|
|
1147
1486
|
});
|
|
1148
1487
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
@@ -1160,25 +1499,127 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1160
1499
|
currentY += result.height;
|
|
1161
1500
|
}
|
|
1162
1501
|
if (showLabels.font && position.font) {
|
|
1163
|
-
// Render "Font: " với font mặc định
|
|
1164
1502
|
const prefix = "Font: ";
|
|
1165
|
-
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1166
|
-
const prefixWidth = ctx.measureText(prefix).width;
|
|
1167
|
-
let currentX = x + prefixWidth;
|
|
1168
|
-
ctx.fillText(prefix, x, currentY);
|
|
1169
|
-
// Render tên font với font từ config
|
|
1170
|
-
ctx.font = `${otherFontSize}px ${position.font}`;
|
|
1171
|
-
const fontNameWidth = ctx.measureText(position.font).width;
|
|
1172
|
-
ctx.fillText(position.font, currentX, currentY);
|
|
1173
|
-
currentX += fontNameWidth;
|
|
1174
|
-
// Render "(Mặc định)" hoặc "(Custom)" với font mặc định
|
|
1175
1503
|
const suffix = position.is_font_default === true ? " (Mặc định)" : " (Custom)";
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
ctx.fillText(suffix, currentX, currentY);
|
|
1179
|
-
// Tính toán height và di chuyển cursorY
|
|
1504
|
+
const fontName = position.font;
|
|
1505
|
+
const fullText = `${prefix}${fontName}${suffix}`;
|
|
1180
1506
|
const lineHeight = otherFontSize + lineGap;
|
|
1181
|
-
|
|
1507
|
+
const textTopY = currentY;
|
|
1508
|
+
const effectiveMaxWidth = mockupBounds
|
|
1509
|
+
? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
|
|
1510
|
+
: maxWidth;
|
|
1511
|
+
const MIN_FONT_FONT_SIZE = otherFontSize * 0.5;
|
|
1512
|
+
const measureFontTextWidth = (fontSize) => {
|
|
1513
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1514
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
1515
|
+
ctx.font = `${fontSize}px ${position.font}`;
|
|
1516
|
+
const fontNameWidth = ctx.measureText(fontName).width;
|
|
1517
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1518
|
+
const suffixWidth = ctx.measureText(suffix).width;
|
|
1519
|
+
return prefixWidth + fontNameWidth + suffixWidth;
|
|
1520
|
+
};
|
|
1521
|
+
const checkFontSizeFits = (fontSize) => {
|
|
1522
|
+
const textTopY = currentY;
|
|
1523
|
+
const lineHeight = fontSize + lineGap;
|
|
1524
|
+
const effectiveMaxWidth = mockupBounds
|
|
1525
|
+
? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
|
|
1526
|
+
: maxWidth;
|
|
1527
|
+
const textWidth = measureFontTextWidth(fontSize);
|
|
1528
|
+
return textWidth <= effectiveMaxWidth;
|
|
1529
|
+
};
|
|
1530
|
+
let effectiveFontSize = otherFontSize;
|
|
1531
|
+
let needsWrap = false;
|
|
1532
|
+
// Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
|
|
1533
|
+
const baseMaxWidth = measureFontTextWidth(otherFontSize);
|
|
1534
|
+
if (baseMaxWidth > effectiveMaxWidth) {
|
|
1535
|
+
// Binary search để tìm font size lớn nhất mà vẫn vừa
|
|
1536
|
+
let left = MIN_FONT_FONT_SIZE;
|
|
1537
|
+
let right = otherFontSize;
|
|
1538
|
+
let bestFontSize = MIN_FONT_FONT_SIZE;
|
|
1539
|
+
while (right - left > 0.1) {
|
|
1540
|
+
const mid = (left + right) / 2;
|
|
1541
|
+
if (checkFontSizeFits(mid)) {
|
|
1542
|
+
bestFontSize = mid;
|
|
1543
|
+
left = mid;
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
right = mid;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (checkFontSizeFits(bestFontSize)) {
|
|
1550
|
+
// Bước 1 thành công: font size đã shrink vừa chiều ngang
|
|
1551
|
+
effectiveFontSize = Math.floor(bestFontSize);
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
// Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
|
|
1555
|
+
needsWrap = true;
|
|
1556
|
+
effectiveFontSize = otherFontSize;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (needsWrap) {
|
|
1560
|
+
// Bước 2: Xuống dòng với font size gốc
|
|
1561
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1562
|
+
const wrappedLines = buildWrappedLines(ctx, fullText, effectiveMaxWidth, x, textTopY, lineHeight, mockupBounds);
|
|
1563
|
+
wrappedLines.forEach((line, index) => {
|
|
1564
|
+
const lineY = textTopY + index * lineHeight;
|
|
1565
|
+
const lineEffectiveMaxWidth = mockupBounds
|
|
1566
|
+
? getEffectiveMaxWidth(x, lineY, lineHeight, maxWidth, mockupBounds)
|
|
1567
|
+
: maxWidth;
|
|
1568
|
+
let lineToRender = line;
|
|
1569
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1570
|
+
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1571
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1572
|
+
lineToRender.length > 0) {
|
|
1573
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
ctx.fillText(lineToRender, x, lineY);
|
|
1577
|
+
});
|
|
1578
|
+
currentY += wrappedLines.length * lineHeight;
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
// Bước 1 thành công: Render với font size đã shrink (1 dòng)
|
|
1582
|
+
const shrunkTextTopY = currentY;
|
|
1583
|
+
const shrunkLineHeight = effectiveFontSize + lineGap;
|
|
1584
|
+
const shrunkEffectiveMaxWidth = mockupBounds
|
|
1585
|
+
? getEffectiveMaxWidth(x, shrunkTextTopY, shrunkLineHeight, maxWidth, mockupBounds)
|
|
1586
|
+
: maxWidth;
|
|
1587
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1588
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
1589
|
+
let currentX = x + prefixWidth;
|
|
1590
|
+
ctx.fillText(prefix, x, currentY);
|
|
1591
|
+
ctx.font = `${effectiveFontSize}px ${position.font}`;
|
|
1592
|
+
const fontNameWidth = ctx.measureText(fontName).width;
|
|
1593
|
+
const totalWidth = prefixWidth + fontNameWidth;
|
|
1594
|
+
if (totalWidth > shrunkEffectiveMaxWidth) {
|
|
1595
|
+
// Cần cắt font name
|
|
1596
|
+
let truncatedFontName = fontName;
|
|
1597
|
+
while (ctx.measureText(truncatedFontName).width >
|
|
1598
|
+
shrunkEffectiveMaxWidth - prefixWidth &&
|
|
1599
|
+
truncatedFontName.length > 0) {
|
|
1600
|
+
truncatedFontName = truncatedFontName.slice(0, -1);
|
|
1601
|
+
}
|
|
1602
|
+
ctx.fillText(truncatedFontName, currentX, currentY);
|
|
1603
|
+
currentX += ctx.measureText(truncatedFontName).width;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
ctx.fillText(fontName, currentX, currentY);
|
|
1607
|
+
currentX += fontNameWidth;
|
|
1608
|
+
}
|
|
1609
|
+
ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1610
|
+
const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
|
|
1611
|
+
if (remainingWidth > 0) {
|
|
1612
|
+
let truncatedSuffix = suffix;
|
|
1613
|
+
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1614
|
+
truncatedSuffix.length > 0) {
|
|
1615
|
+
truncatedSuffix = truncatedSuffix.slice(0, -1);
|
|
1616
|
+
}
|
|
1617
|
+
if (truncatedSuffix.length > 0) {
|
|
1618
|
+
ctx.fillText(truncatedSuffix, currentX, currentY);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
currentY += shrunkLineHeight;
|
|
1622
|
+
}
|
|
1182
1623
|
}
|
|
1183
1624
|
if (showLabels.color) {
|
|
1184
1625
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1211,7 +1652,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1211
1652
|
else {
|
|
1212
1653
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1213
1654
|
swatchX = swatchesStartX;
|
|
1214
|
-
swatchY =
|
|
1655
|
+
swatchY =
|
|
1656
|
+
result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1215
1657
|
currentY += result.height;
|
|
1216
1658
|
}
|
|
1217
1659
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -1320,130 +1762,122 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1320
1762
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1321
1763
|
const iconHeight = iconFontSize * 2;
|
|
1322
1764
|
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1323
|
-
iconImageReservedWidth =
|
|
1765
|
+
iconImageReservedWidth =
|
|
1766
|
+
iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1324
1767
|
}
|
|
1325
1768
|
}
|
|
1326
1769
|
}
|
|
1327
|
-
// Tính available width cho icon value (trừ đi khoảng trống cho icon_image)
|
|
1328
1770
|
const availableWidth = Math.max(1, maxWidth - labelWidth - iconImageReservedWidth);
|
|
1329
|
-
|
|
1330
|
-
|
|
1771
|
+
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1772
|
+
const valueStartX = x + labelWidth;
|
|
1773
|
+
const textTopY = textCenterY - iconFontSize / 2;
|
|
1774
|
+
const lineHeight = iconFontSize;
|
|
1775
|
+
const effectiveMaxWidth = mockupBounds
|
|
1776
|
+
? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
|
|
1777
|
+
: availableWidth;
|
|
1331
1778
|
const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
|
|
1332
1779
|
const measureIconValueWidth = (fontSize) => {
|
|
1333
1780
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1334
1781
|
return ctx.measureText(` ${iconValue}`).width;
|
|
1335
1782
|
};
|
|
1783
|
+
const checkFontSizeFits = (fontSize) => {
|
|
1784
|
+
const textTopY = textCenterY - fontSize / 2;
|
|
1785
|
+
const lineHeight = fontSize;
|
|
1786
|
+
const effectiveMaxWidth = mockupBounds
|
|
1787
|
+
? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
|
|
1788
|
+
: availableWidth;
|
|
1789
|
+
const textWidth = measureIconValueWidth(fontSize);
|
|
1790
|
+
return textWidth <= effectiveMaxWidth;
|
|
1791
|
+
};
|
|
1336
1792
|
let effectiveIconValueFontSize = iconFontSize;
|
|
1337
|
-
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1338
1793
|
let needsWrap = false;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
//
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1794
|
+
// Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
|
|
1795
|
+
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1796
|
+
if (baseMaxWidth > effectiveMaxWidth) {
|
|
1797
|
+
// Binary search để tìm font size lớn nhất mà vẫn vừa
|
|
1798
|
+
let left = MIN_ICON_VALUE_FONT_SIZE;
|
|
1799
|
+
let right = iconFontSize;
|
|
1800
|
+
let bestFontSize = MIN_ICON_VALUE_FONT_SIZE;
|
|
1801
|
+
while (right - left > 0.1) {
|
|
1802
|
+
const mid = (left + right) / 2;
|
|
1803
|
+
if (checkFontSizeFits(mid)) {
|
|
1804
|
+
bestFontSize = mid;
|
|
1805
|
+
left = mid;
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
right = mid;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (checkFontSizeFits(bestFontSize)) {
|
|
1812
|
+
// Bước 1 thành công: font size đã shrink vừa chiều ngang
|
|
1813
|
+
effectiveIconValueFontSize = Math.floor(bestFontSize);
|
|
1814
|
+
}
|
|
1815
|
+
else {
|
|
1816
|
+
// Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
|
|
1346
1817
|
needsWrap = true;
|
|
1347
|
-
effectiveIconValueFontSize =
|
|
1818
|
+
effectiveIconValueFontSize = iconFontSize;
|
|
1348
1819
|
}
|
|
1349
1820
|
}
|
|
1350
|
-
//
|
|
1351
|
-
const valueLineHeight = effectiveIconValueFontSize;
|
|
1352
|
-
let allWrappedLines = [];
|
|
1353
|
-
// Text align center: căn giữa theo chiều dọc trong block
|
|
1354
|
-
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1355
|
-
const valueStartX = x + labelWidth;
|
|
1356
|
-
if (needsWrap) {
|
|
1357
|
-
// Dùng wrap text logic
|
|
1358
|
-
const wrappedLines = buildWrappedLines(ctx, iconValue, availableWidth, valueStartX, textCenterY, valueLineHeight, mockupBounds);
|
|
1359
|
-
allWrappedLines = wrappedLines;
|
|
1360
|
-
wrappedLines.length * valueLineHeight;
|
|
1361
|
-
}
|
|
1362
|
-
else {
|
|
1363
|
-
// Không cần wrap, chỉ một dòng
|
|
1364
|
-
allWrappedLines = [iconValue];
|
|
1365
|
-
}
|
|
1366
|
-
// Vẽ label với textBaseline = middle để align center với value
|
|
1821
|
+
// Bước 2: Nếu bước 1 thất bại, xuống dòng với font size gốc
|
|
1367
1822
|
ctx.textBaseline = "middle";
|
|
1368
1823
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1369
1824
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1370
1825
|
ctx.fillText(iconLabel, x, textCenterY);
|
|
1371
|
-
// Vẽ icon value với align center
|
|
1372
1826
|
ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1373
1827
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1374
1828
|
let maxValueLineWidth = 0;
|
|
1375
|
-
|
|
1829
|
+
let totalTextHeight = iconImageHeight;
|
|
1376
1830
|
if (needsWrap) {
|
|
1377
|
-
//
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
maxValueLineWidth = w;
|
|
1394
|
-
}
|
|
1395
|
-
else {
|
|
1396
|
-
ctx.fillText(line, valueStartX, lineY);
|
|
1397
|
-
const w = ctx.measureText(line).width;
|
|
1398
|
-
if (w > maxValueLineWidth)
|
|
1399
|
-
maxValueLineWidth = w;
|
|
1831
|
+
// Bước 2: Xuống dòng với font size gốc
|
|
1832
|
+
const wrappedLines = buildWrappedLines(ctx, iconValue, effectiveMaxWidth, valueStartX, textTopY, lineHeight, mockupBounds);
|
|
1833
|
+
// Tính height dựa trên số dòng thực tế
|
|
1834
|
+
totalTextHeight = Math.max(iconImageHeight, wrappedLines.length * lineHeight);
|
|
1835
|
+
wrappedLines.forEach((line, index) => {
|
|
1836
|
+
const lineTopY = textTopY + index * lineHeight;
|
|
1837
|
+
const lineCenterY = lineTopY + lineHeight / 2;
|
|
1838
|
+
const lineEffectiveMaxWidth = mockupBounds
|
|
1839
|
+
? getEffectiveMaxWidth(valueStartX, lineTopY, lineHeight, availableWidth, mockupBounds)
|
|
1840
|
+
: availableWidth;
|
|
1841
|
+
let lineToRender = line;
|
|
1842
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1843
|
+
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1844
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1845
|
+
lineToRender.length > 0) {
|
|
1846
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1400
1847
|
}
|
|
1401
1848
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
maxValueLineWidth = w;
|
|
1407
|
-
}
|
|
1849
|
+
ctx.fillText(lineToRender, valueStartX, lineCenterY);
|
|
1850
|
+
const w = ctx.measureText(lineToRender).width;
|
|
1851
|
+
if (w > maxValueLineWidth)
|
|
1852
|
+
maxValueLineWidth = w;
|
|
1408
1853
|
});
|
|
1409
1854
|
}
|
|
1410
1855
|
else {
|
|
1411
|
-
//
|
|
1856
|
+
// Bước 1 thành công: Render với font size đã shrink (1 dòng)
|
|
1857
|
+
const shrunkTextTopY = textCenterY - effectiveIconValueFontSize / 2;
|
|
1858
|
+
const shrunkLineHeight = effectiveIconValueFontSize;
|
|
1859
|
+
const shrunkEffectiveMaxWidth = mockupBounds
|
|
1860
|
+
? getEffectiveMaxWidth(valueStartX, shrunkTextTopY, shrunkLineHeight, availableWidth, mockupBounds)
|
|
1861
|
+
: availableWidth;
|
|
1412
1862
|
const lineText = ` ${iconValue}`;
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
let truncatedLine = lineText;
|
|
1420
|
-
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1421
|
-
truncatedLine = truncatedLine.slice(0, -1);
|
|
1422
|
-
}
|
|
1423
|
-
ctx.fillText(truncatedLine, valueStartX, textCenterY);
|
|
1424
|
-
const w = ctx.measureText(truncatedLine).width;
|
|
1425
|
-
if (w > maxValueLineWidth)
|
|
1426
|
-
maxValueLineWidth = w;
|
|
1427
|
-
}
|
|
1428
|
-
else {
|
|
1429
|
-
ctx.fillText(lineText, valueStartX, textCenterY);
|
|
1430
|
-
const w = ctx.measureText(lineText).width;
|
|
1431
|
-
if (w > maxValueLineWidth)
|
|
1432
|
-
maxValueLineWidth = w;
|
|
1863
|
+
let lineToRender = lineText;
|
|
1864
|
+
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1865
|
+
if (lineWidth > shrunkEffectiveMaxWidth) {
|
|
1866
|
+
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1867
|
+
lineToRender.length > 0) {
|
|
1868
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
1433
1869
|
}
|
|
1434
1870
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
maxValueLineWidth = w;
|
|
1440
|
-
}
|
|
1871
|
+
ctx.fillText(lineToRender, valueStartX, textCenterY);
|
|
1872
|
+
const w = ctx.measureText(lineToRender).width;
|
|
1873
|
+
if (w > maxValueLineWidth)
|
|
1874
|
+
maxValueLineWidth = w;
|
|
1441
1875
|
}
|
|
1442
1876
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1443
1877
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1444
1878
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1445
1879
|
const iconResult = {
|
|
1446
|
-
height:
|
|
1880
|
+
height: totalTextHeight + lineGap,
|
|
1447
1881
|
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1448
1882
|
lastLineWidth: labelWidth + maxValueLineWidth};
|
|
1449
1883
|
// Kiểm tra xem phần icon image có vượt quá canvas không trước khi render
|
|
@@ -1493,7 +1927,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1493
1927
|
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1494
1928
|
testLines.length * lineHeight;
|
|
1495
1929
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1496
|
-
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1930
|
+
const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
|
|
1497
1931
|
const textEndX = x + testTextWidth;
|
|
1498
1932
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1499
1933
|
const swatchesStartX = textEndX + spacing;
|
|
@@ -1522,7 +1956,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1522
1956
|
else {
|
|
1523
1957
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1524
1958
|
swatchX = swatchesStartX;
|
|
1525
|
-
swatchY =
|
|
1959
|
+
swatchY =
|
|
1960
|
+
colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1526
1961
|
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1527
1962
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1528
1963
|
// cursorY đã được cập nhật ở trên
|
|
@@ -1538,10 +1973,7 @@ const prepareExportCanvas = async (config, options = {}) => {
|
|
|
1538
1973
|
current: new Map(),
|
|
1539
1974
|
};
|
|
1540
1975
|
// Load fonts and images in parallel
|
|
1541
|
-
await Promise.all([
|
|
1542
|
-
preloadFonts(config),
|
|
1543
|
-
preloadImages(config, imageRefs)
|
|
1544
|
-
]);
|
|
1976
|
+
await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
|
|
1545
1977
|
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
1546
1978
|
if (!canvas.width || !canvas.height) {
|
|
1547
1979
|
return null;
|