embroidery-qc-image 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/EmbroideryQCImage.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +387 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +387 -42
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +6 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwhBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAqkEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React$1 from 'react';
|
|
2
2
|
|
|
3
3
|
interface TextPosition {
|
|
4
|
-
type:
|
|
4
|
+
type: "TEXT";
|
|
5
5
|
text: string;
|
|
6
6
|
text_shape?: string | null;
|
|
7
7
|
color?: string | null;
|
|
@@ -9,9 +9,11 @@ interface TextPosition {
|
|
|
9
9
|
is_font_default?: boolean | null;
|
|
10
10
|
character_colors?: string[] | null;
|
|
11
11
|
floral_pattern?: string | null;
|
|
12
|
+
layer_colors?: string[] | null;
|
|
13
|
+
attachment?: string | null;
|
|
12
14
|
}
|
|
13
15
|
interface IconPosition {
|
|
14
|
-
type:
|
|
16
|
+
type: "ICON";
|
|
15
17
|
icon: number;
|
|
16
18
|
icon_name?: string | null;
|
|
17
19
|
icon_image?: string | null;
|
|
@@ -24,6 +26,8 @@ type Position = TextPosition | IconPosition;
|
|
|
24
26
|
interface Side {
|
|
25
27
|
print_side: string;
|
|
26
28
|
item_type?: string | null;
|
|
29
|
+
link?: string | null;
|
|
30
|
+
size?: string | null;
|
|
27
31
|
positions: Position[];
|
|
28
32
|
}
|
|
29
33
|
interface EmbroideryQCConfig {
|
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
|
|
@@ -1229,7 +1568,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1229
1568
|
let lineToRender = line;
|
|
1230
1569
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1231
1570
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1232
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1571
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1572
|
+
lineToRender.length > 0) {
|
|
1233
1573
|
lineToRender = lineToRender.slice(0, -1);
|
|
1234
1574
|
}
|
|
1235
1575
|
}
|
|
@@ -1254,7 +1594,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1254
1594
|
if (totalWidth > shrunkEffectiveMaxWidth) {
|
|
1255
1595
|
// Cần cắt font name
|
|
1256
1596
|
let truncatedFontName = fontName;
|
|
1257
|
-
while (ctx.measureText(truncatedFontName).width >
|
|
1597
|
+
while (ctx.measureText(truncatedFontName).width >
|
|
1598
|
+
shrunkEffectiveMaxWidth - prefixWidth &&
|
|
1599
|
+
truncatedFontName.length > 0) {
|
|
1258
1600
|
truncatedFontName = truncatedFontName.slice(0, -1);
|
|
1259
1601
|
}
|
|
1260
1602
|
ctx.fillText(truncatedFontName, currentX, currentY);
|
|
@@ -1268,7 +1610,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1268
1610
|
const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
|
|
1269
1611
|
if (remainingWidth > 0) {
|
|
1270
1612
|
let truncatedSuffix = suffix;
|
|
1271
|
-
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1613
|
+
while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
|
|
1614
|
+
truncatedSuffix.length > 0) {
|
|
1272
1615
|
truncatedSuffix = truncatedSuffix.slice(0, -1);
|
|
1273
1616
|
}
|
|
1274
1617
|
if (truncatedSuffix.length > 0) {
|
|
@@ -1309,7 +1652,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
|
1309
1652
|
else {
|
|
1310
1653
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1311
1654
|
swatchX = swatchesStartX;
|
|
1312
|
-
swatchY =
|
|
1655
|
+
swatchY =
|
|
1656
|
+
result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1313
1657
|
currentY += result.height;
|
|
1314
1658
|
}
|
|
1315
1659
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -1418,7 +1762,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1418
1762
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1419
1763
|
const iconHeight = iconFontSize * 2;
|
|
1420
1764
|
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1421
|
-
iconImageReservedWidth =
|
|
1765
|
+
iconImageReservedWidth =
|
|
1766
|
+
iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1422
1767
|
}
|
|
1423
1768
|
}
|
|
1424
1769
|
}
|
|
@@ -1496,7 +1841,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1496
1841
|
let lineToRender = line;
|
|
1497
1842
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1498
1843
|
if (lineWidth > lineEffectiveMaxWidth) {
|
|
1499
|
-
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1844
|
+
while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
|
|
1845
|
+
lineToRender.length > 0) {
|
|
1500
1846
|
lineToRender = lineToRender.slice(0, -1);
|
|
1501
1847
|
}
|
|
1502
1848
|
}
|
|
@@ -1517,7 +1863,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1517
1863
|
let lineToRender = lineText;
|
|
1518
1864
|
const lineWidth = ctx.measureText(lineToRender).width;
|
|
1519
1865
|
if (lineWidth > shrunkEffectiveMaxWidth) {
|
|
1520
|
-
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1866
|
+
while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
|
|
1867
|
+
lineToRender.length > 0) {
|
|
1521
1868
|
lineToRender = lineToRender.slice(0, -1);
|
|
1522
1869
|
}
|
|
1523
1870
|
}
|
|
@@ -1580,7 +1927,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1580
1927
|
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1581
1928
|
testLines.length * lineHeight;
|
|
1582
1929
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1583
|
-
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1930
|
+
const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
|
|
1584
1931
|
const textEndX = x + testTextWidth;
|
|
1585
1932
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1586
1933
|
const swatchesStartX = textEndX + spacing;
|
|
@@ -1609,7 +1956,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1609
1956
|
else {
|
|
1610
1957
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1611
1958
|
swatchX = swatchesStartX;
|
|
1612
|
-
swatchY =
|
|
1959
|
+
swatchY =
|
|
1960
|
+
colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1613
1961
|
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1614
1962
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1615
1963
|
// cursorY đã được cập nhật ở trên
|
|
@@ -1625,10 +1973,7 @@ const prepareExportCanvas = async (config, options = {}) => {
|
|
|
1625
1973
|
current: new Map(),
|
|
1626
1974
|
};
|
|
1627
1975
|
// Load fonts and images in parallel
|
|
1628
|
-
await Promise.all([
|
|
1629
|
-
preloadFonts(config),
|
|
1630
|
-
preloadImages(config, imageRefs)
|
|
1631
|
-
]);
|
|
1976
|
+
await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
|
|
1632
1977
|
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
1633
1978
|
if (!canvas.width || !canvas.height) {
|
|
1634
1979
|
return null;
|