embroidery-qc-image 1.0.23 → 1.0.25
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 +5 -0
- package/dist/index.esm.js +234 -185
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +234 -185
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +5 -0
- 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;AAibD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAugCF,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
|
@@ -13,6 +13,9 @@ interface TextPosition {
|
|
|
13
13
|
interface IconPosition {
|
|
14
14
|
type: 'ICON';
|
|
15
15
|
icon: number;
|
|
16
|
+
icon_name?: string | null;
|
|
17
|
+
icon_image?: string | null;
|
|
18
|
+
is_delete_icon?: boolean | null;
|
|
16
19
|
note?: string | null;
|
|
17
20
|
color?: string | null;
|
|
18
21
|
layer_colors: string[];
|
|
@@ -20,10 +23,12 @@ interface IconPosition {
|
|
|
20
23
|
type Position = TextPosition | IconPosition;
|
|
21
24
|
interface Side {
|
|
22
25
|
print_side: string;
|
|
26
|
+
item_type?: string | null;
|
|
23
27
|
positions: Position[];
|
|
24
28
|
}
|
|
25
29
|
interface EmbroideryQCConfig {
|
|
26
30
|
image_url?: string;
|
|
31
|
+
message?: string | null;
|
|
27
32
|
error_message?: string | null;
|
|
28
33
|
warning_message?: string | null;
|
|
29
34
|
sides: Side[];
|
package/dist/index.esm.js
CHANGED
|
@@ -31,71 +31,6 @@ 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
|
-
};
|
|
99
34
|
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
100
35
|
const DEFAULT_WARNING_COLOR = "#FF8C00";
|
|
101
36
|
const BASE_URLS = {
|
|
@@ -111,7 +46,7 @@ const LAYOUT = {
|
|
|
111
46
|
// Font sizes (base values, will be multiplied by scaleFactor)
|
|
112
47
|
HEADER_FONT_SIZE: 220,
|
|
113
48
|
TEXT_FONT_SIZE: 200,
|
|
114
|
-
OTHER_FONT_SIZE:
|
|
49
|
+
OTHER_FONT_SIZE: 180,
|
|
115
50
|
// Colors
|
|
116
51
|
HEADER_COLOR: "#000000",
|
|
117
52
|
LABEL_COLOR: "#444444",
|
|
@@ -120,12 +55,12 @@ const LAYOUT = {
|
|
|
120
55
|
TEXT_ALIGN: "left",
|
|
121
56
|
TEXT_BASELINE: "top",
|
|
122
57
|
// Spacing
|
|
123
|
-
LINE_GAP:
|
|
124
|
-
PADDING:
|
|
58
|
+
LINE_GAP: 50,
|
|
59
|
+
PADDING: 50,
|
|
125
60
|
SECTION_SPACING: 60,
|
|
126
61
|
ELEMENT_SPACING: 100,
|
|
127
62
|
SWATCH_SPACING: 25,
|
|
128
|
-
FLORAL_SPACING:
|
|
63
|
+
FLORAL_SPACING: 100,
|
|
129
64
|
// Visual styling
|
|
130
65
|
SWATCH_HEIGHT_RATIO: 2.025,
|
|
131
66
|
UNDERLINE_POSITION: 0.9,
|
|
@@ -168,6 +103,17 @@ const getImageUrl = (type, value) => {
|
|
|
168
103
|
return `${BASE_URLS.THREAD_COLOR}/${value}.webp`;
|
|
169
104
|
};
|
|
170
105
|
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
106
|
+
const getIconImageUrl = (position) => {
|
|
107
|
+
if (position.is_delete_icon)
|
|
108
|
+
return null;
|
|
109
|
+
if (position.icon_image && position.icon_image.trim().length > 0) {
|
|
110
|
+
return position.icon_image;
|
|
111
|
+
}
|
|
112
|
+
if (position.icon !== 0) {
|
|
113
|
+
return getImageUrl("icon", position.icon);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
171
117
|
const ensureImage = (existing) => {
|
|
172
118
|
if (existing && existing.crossOrigin === "anonymous") {
|
|
173
119
|
return existing;
|
|
@@ -199,14 +145,14 @@ const loadImage = (url, imageRefs, onLoad) => {
|
|
|
199
145
|
img.onerror = () => {
|
|
200
146
|
if (!attemptedProxy) {
|
|
201
147
|
attemptedProxy = true;
|
|
202
|
-
img.src = getProxyUrl(url);
|
|
148
|
+
img.src = getProxyUrl(getResizeUrl(url));
|
|
203
149
|
return;
|
|
204
150
|
}
|
|
205
151
|
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
206
152
|
cleanup();
|
|
207
153
|
onLoad();
|
|
208
154
|
};
|
|
209
|
-
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
155
|
+
img.src = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
210
156
|
};
|
|
211
157
|
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
212
158
|
const key = cacheKey ?? url;
|
|
@@ -244,13 +190,13 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
244
190
|
target.onerror = () => {
|
|
245
191
|
if (!attemptedProxy) {
|
|
246
192
|
attemptedProxy = true;
|
|
247
|
-
target.src = getProxyUrl(url);
|
|
193
|
+
target.src = getProxyUrl(getResizeUrl(url));
|
|
248
194
|
return;
|
|
249
195
|
}
|
|
250
196
|
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
251
197
|
finalize();
|
|
252
198
|
};
|
|
253
|
-
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
199
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
254
200
|
if (target.src !== desiredSrc) {
|
|
255
201
|
target.src = desiredSrc;
|
|
256
202
|
}
|
|
@@ -259,6 +205,57 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
259
205
|
}
|
|
260
206
|
});
|
|
261
207
|
};
|
|
208
|
+
const getResizeUrl = (url) => {
|
|
209
|
+
try {
|
|
210
|
+
const urlObj = new URL(url);
|
|
211
|
+
// Xử lý cdn.shopify.com
|
|
212
|
+
if (urlObj.hostname === 'cdn.shopify.com') {
|
|
213
|
+
// Set hoặc update query param width=400
|
|
214
|
+
urlObj.searchParams.set('width', '400');
|
|
215
|
+
return urlObj.toString();
|
|
216
|
+
}
|
|
217
|
+
// Xử lý m.media-amazon.com
|
|
218
|
+
if (urlObj.hostname === 'm.media-amazon.com') {
|
|
219
|
+
const pathname = urlObj.pathname;
|
|
220
|
+
// Split pathname theo dấu /
|
|
221
|
+
const pathArr = pathname.split('/');
|
|
222
|
+
// Lấy filename (phần cuối cùng)
|
|
223
|
+
const filename = pathArr[pathArr.length - 1];
|
|
224
|
+
// Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
|
|
225
|
+
const cleanedFilename = filename.replace(/\._.*_/g, '');
|
|
226
|
+
// Split filename đã clean theo dấu .
|
|
227
|
+
const parts = cleanedFilename.split('.');
|
|
228
|
+
if (parts.length >= 2) {
|
|
229
|
+
// Lấy phần đầu và phần cuối
|
|
230
|
+
const firstPart = parts[0];
|
|
231
|
+
const lastPart = parts[parts.length - 1];
|
|
232
|
+
// Chèn _AC_SX400_ vào giữa và join lại
|
|
233
|
+
const newFilename = `${firstPart}._AC_SX400_.${lastPart}`;
|
|
234
|
+
// Thay filename mới vào pathArr
|
|
235
|
+
pathArr[pathArr.length - 1] = newFilename;
|
|
236
|
+
// Join lại
|
|
237
|
+
urlObj.pathname = pathArr.join('/');
|
|
238
|
+
return urlObj.toString();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Xử lý i.etsystatic.com
|
|
242
|
+
if (urlObj.hostname === 'i.etsystatic.com') {
|
|
243
|
+
const pathname = urlObj.pathname;
|
|
244
|
+
// Thay il_fullxfull bằng il_400x400
|
|
245
|
+
if (pathname.includes('il_fullxfull')) {
|
|
246
|
+
const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
|
|
247
|
+
urlObj.pathname = newPathname;
|
|
248
|
+
return urlObj.toString();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Nếu không phải các domain cần xử lý, return URL gốc
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// Nếu URL không hợp lệ, return URL gốc
|
|
256
|
+
return url;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
262
259
|
const preloadFonts = async (config) => {
|
|
263
260
|
if (config.error_message || !config.sides?.length)
|
|
264
261
|
return;
|
|
@@ -288,12 +285,10 @@ const preloadImages = async (config, imageRefs) => {
|
|
|
288
285
|
config.sides.forEach((side) => {
|
|
289
286
|
side.positions.forEach((position) => {
|
|
290
287
|
if (position.type === "ICON") {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
seen.add(iconUrl);
|
|
296
|
-
}
|
|
288
|
+
const iconUrl = getIconImageUrl(position);
|
|
289
|
+
if (iconUrl && !seen.has(iconUrl)) {
|
|
290
|
+
entries.push({ url: iconUrl });
|
|
291
|
+
seen.add(iconUrl);
|
|
297
292
|
}
|
|
298
293
|
if (position.color) {
|
|
299
294
|
const threadUrl = getImageUrl("threadColor", position.color);
|
|
@@ -366,60 +361,30 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
366
361
|
};
|
|
367
362
|
};
|
|
368
363
|
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
currentLine = words[i];
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
currentLine = testLine;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
lines.push(currentLine);
|
|
385
|
-
return lines;
|
|
386
|
-
};
|
|
387
|
-
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
388
|
-
const words = text.split(" ");
|
|
389
|
-
const lines = [];
|
|
390
|
-
const lineStartIndices = [0];
|
|
391
|
-
let currentLine = words[0];
|
|
392
|
-
let currentCharIndex = words[0].length;
|
|
393
|
-
for (let i = 1; i < words.length; i++) {
|
|
394
|
-
const testLine = currentLine + " " + words[i];
|
|
395
|
-
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
396
|
-
lines.push(currentLine);
|
|
397
|
-
lineStartIndices.push(currentCharIndex + 1);
|
|
398
|
-
currentLine = words[i];
|
|
399
|
-
currentCharIndex += words[i].length + 1;
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
currentLine = testLine;
|
|
403
|
-
currentCharIndex += words[i].length + 1;
|
|
364
|
+
// Mỗi '\n' tương đương với một line break giống như khi wrap tự động.
|
|
365
|
+
const segments = text.split("\n");
|
|
366
|
+
const result = [];
|
|
367
|
+
segments.forEach((segment) => {
|
|
368
|
+
const words = segment.split(" ").filter((word) => word.length > 0);
|
|
369
|
+
if (words.length === 0) {
|
|
370
|
+
// Nếu đoạn rỗng, thêm một dòng trống (break đúng 1 line)
|
|
371
|
+
result.push("");
|
|
372
|
+
return;
|
|
404
373
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const color = colors[colorIndex];
|
|
416
|
-
ctx.fillStyle = COLOR_MAP[color] || LAYOUT.LABEL_COLOR;
|
|
417
|
-
ctx.fillText(char, currentX, currentY);
|
|
418
|
-
currentX += ctx.measureText(char).width;
|
|
374
|
+
let currentLine = words[0];
|
|
375
|
+
for (let i = 1; i < words.length; i++) {
|
|
376
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
377
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
378
|
+
result.push(currentLine);
|
|
379
|
+
currentLine = words[i];
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
currentLine = testLine;
|
|
383
|
+
}
|
|
419
384
|
}
|
|
420
|
-
|
|
385
|
+
result.push(currentLine);
|
|
421
386
|
});
|
|
422
|
-
return
|
|
387
|
+
return result.length ? result : [""];
|
|
423
388
|
};
|
|
424
389
|
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
425
390
|
let swatchX = startX;
|
|
@@ -483,8 +448,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
483
448
|
config.sides.forEach((side) => {
|
|
484
449
|
side.positions.forEach((position) => {
|
|
485
450
|
if (position.type === "ICON") {
|
|
486
|
-
|
|
487
|
-
|
|
451
|
+
const iconUrl = getIconImageUrl(position);
|
|
452
|
+
if (iconUrl) {
|
|
453
|
+
loadImage(iconUrl, imageRefs, incrementCounter);
|
|
488
454
|
}
|
|
489
455
|
position.layer_colors?.forEach((color) => {
|
|
490
456
|
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
@@ -535,7 +501,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
535
501
|
return;
|
|
536
502
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
537
503
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
538
|
-
// Calculate warning height (with scaleFactor = 1 for measurement)
|
|
504
|
+
// Calculate warning & message height (with scaleFactor = 1 for measurement)
|
|
539
505
|
let warningHeight = 0;
|
|
540
506
|
let warningLineHeight = 0;
|
|
541
507
|
let warningLineCount = 0;
|
|
@@ -554,6 +520,24 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
554
520
|
warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
|
|
555
521
|
}
|
|
556
522
|
}
|
|
523
|
+
let messageHeight = 0;
|
|
524
|
+
let messageLineHeight = 0;
|
|
525
|
+
let messageLineCount = 0;
|
|
526
|
+
if (config.message) {
|
|
527
|
+
const measureMessageCanvas = document.createElement("canvas");
|
|
528
|
+
measureMessageCanvas.width = canvas.width;
|
|
529
|
+
measureMessageCanvas.height = canvas.height;
|
|
530
|
+
const measureMessageCtx = measureMessageCanvas.getContext("2d");
|
|
531
|
+
if (measureMessageCtx) {
|
|
532
|
+
measureMessageCtx.textAlign = "left";
|
|
533
|
+
measureMessageCtx.textBaseline = "top";
|
|
534
|
+
measureMessageCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
|
|
535
|
+
const messageLines = buildWrappedLines(measureMessageCtx, config.message.trim(), canvas.width - LAYOUT.PADDING * 4);
|
|
536
|
+
messageLineCount = messageLines.length;
|
|
537
|
+
messageLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
|
|
538
|
+
messageHeight = messageLineCount * messageLineHeight + LAYOUT.PADDING;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
557
541
|
if (config.image_url) {
|
|
558
542
|
const mockupImage = imageRefs.current.get(config.image_url);
|
|
559
543
|
if (mockupImage) {
|
|
@@ -586,27 +570,40 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
586
570
|
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
587
571
|
let measureY = LAYOUT.PADDING;
|
|
588
572
|
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
589
|
-
// Add warning text height (without bottom padding, no spacing)
|
|
573
|
+
// Add warning & message text height (without bottom padding, no spacing)
|
|
590
574
|
if (config.warning_message) {
|
|
591
575
|
const warningTextHeight = warningHeight - LAYOUT.PADDING;
|
|
592
576
|
measureY += warningTextHeight;
|
|
593
577
|
}
|
|
578
|
+
if (config.message) {
|
|
579
|
+
const messageTextHeight = messageHeight - LAYOUT.PADDING;
|
|
580
|
+
measureY += messageTextHeight;
|
|
581
|
+
}
|
|
594
582
|
config.sides.forEach((side) => {
|
|
595
583
|
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
596
584
|
measureY += sideHeight + measureSpacing;
|
|
597
585
|
});
|
|
598
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight) / measureY));
|
|
586
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
|
|
599
587
|
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
600
|
-
// Render warning with scaleFactor and get actual
|
|
588
|
+
// Render warning & message with scaleFactor and get actual heights
|
|
601
589
|
let actualWarningHeight = 0;
|
|
590
|
+
let actualMessageHeight = 0;
|
|
602
591
|
if (config.warning_message) {
|
|
603
592
|
actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
|
|
604
593
|
}
|
|
605
|
-
|
|
594
|
+
if (config.message) {
|
|
595
|
+
actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, actualWarningHeight, "", // message: không cần prefix "Note"
|
|
596
|
+
DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
606
600
|
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
607
601
|
if (config.warning_message && actualWarningHeight > 0) {
|
|
608
602
|
currentY += actualWarningHeight;
|
|
609
603
|
}
|
|
604
|
+
if (config.message && actualMessageHeight > 0) {
|
|
605
|
+
currentY += actualMessageHeight;
|
|
606
|
+
}
|
|
610
607
|
config.sides.forEach((side) => {
|
|
611
608
|
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
612
609
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
@@ -648,8 +645,8 @@ const renderErrorState = (ctx, canvas, message) => {
|
|
|
648
645
|
});
|
|
649
646
|
ctx.restore();
|
|
650
647
|
};
|
|
651
|
-
const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
652
|
-
const sanitizedMessage =
|
|
648
|
+
const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefix = "Note: ", color = DEFAULT_WARNING_COLOR) => {
|
|
649
|
+
const sanitizedMessage = `${prefix}${message.trim()}`;
|
|
653
650
|
const horizontalPadding = LAYOUT.PADDING * 2 * scaleFactor;
|
|
654
651
|
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
655
652
|
const baseFontSize = LAYOUT.HEADER_FONT_SIZE * 0.7 * scaleFactor;
|
|
@@ -658,7 +655,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
658
655
|
ctx.save();
|
|
659
656
|
ctx.textAlign = "left";
|
|
660
657
|
ctx.textBaseline = "top";
|
|
661
|
-
ctx.fillStyle =
|
|
658
|
+
ctx.fillStyle = color;
|
|
662
659
|
ctx.font = `${baseFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
663
660
|
let fontSize = baseFontSize;
|
|
664
661
|
let lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -677,7 +674,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
677
674
|
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
678
675
|
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
679
676
|
}
|
|
680
|
-
const startY = LAYOUT.PADDING * scaleFactor;
|
|
677
|
+
const startY = LAYOUT.PADDING * scaleFactor + offsetY;
|
|
681
678
|
lines.forEach((line, index) => {
|
|
682
679
|
const y = startY + index * lineHeight;
|
|
683
680
|
ctx.fillText(line, leftX, y);
|
|
@@ -701,7 +698,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
|
701
698
|
ctx.drawImage(mockupImg, x, y, width, height);
|
|
702
699
|
// Draw florals
|
|
703
700
|
if (floralAssets.length > 0) {
|
|
704
|
-
const floralH = Math.min(
|
|
701
|
+
const floralH = Math.min(500, height);
|
|
705
702
|
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
706
703
|
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
707
704
|
const img = floralAssets[i];
|
|
@@ -892,9 +889,25 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
892
889
|
let rendered = 0;
|
|
893
890
|
if (values.font && shouldRenderField("font")) {
|
|
894
891
|
const allDefault = textPositions.every((p) => p.is_font_default === true);
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
|
|
892
|
+
// Render "Font: " với font mặc định
|
|
893
|
+
const prefix = "Font: ";
|
|
894
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
895
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
896
|
+
let currentX = x + prefixWidth;
|
|
897
|
+
ctx.fillText(prefix, x, cursorY);
|
|
898
|
+
// Render tên font với font từ config
|
|
899
|
+
ctx.font = `${fontSize}px ${values.font}`;
|
|
900
|
+
const fontNameWidth = ctx.measureText(values.font).width;
|
|
901
|
+
ctx.fillText(values.font, currentX, cursorY);
|
|
902
|
+
currentX += fontNameWidth;
|
|
903
|
+
// Render "(Mặc định)" hoặc "(Custom)" với font mặc định
|
|
904
|
+
const suffix = allDefault ? " (Mặc định)" : " (Custom)";
|
|
905
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
906
|
+
ctx.measureText(suffix).width;
|
|
907
|
+
ctx.fillText(suffix, currentX, cursorY);
|
|
908
|
+
// Tính toán height và di chuyển cursorY
|
|
909
|
+
const lineHeight = fontSize + lineGap;
|
|
910
|
+
cursorY += lineHeight;
|
|
898
911
|
rendered++;
|
|
899
912
|
}
|
|
900
913
|
if (values.shape && values.shape !== "None" && shouldRenderField("shape")) {
|
|
@@ -943,31 +956,15 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
943
956
|
const textMaxWidth = maxWidth - labelWidth;
|
|
944
957
|
// Get display text (handle empty/null/undefined)
|
|
945
958
|
const isEmptyText = !position.text || position.text.trim() === "";
|
|
946
|
-
// Draw text content
|
|
959
|
+
// Draw text content - dùng font mặc định và màu đỏ
|
|
960
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
961
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
947
962
|
if (isEmptyText) {
|
|
948
|
-
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
949
|
-
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
950
963
|
const textResult = wrapText(ctx, "(không có text)", x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
951
964
|
currentY += textResult.height;
|
|
952
965
|
drawnHeight += textResult.height;
|
|
953
966
|
}
|
|
954
|
-
else if (position.floral_pattern) {
|
|
955
|
-
// Khi có floral_pattern, dùng màu mặc định như label
|
|
956
|
-
ctx.font = `${textFontSize}px ${position.font}`;
|
|
957
|
-
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
958
|
-
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
959
|
-
currentY += textResult.height;
|
|
960
|
-
drawnHeight += textResult.height;
|
|
961
|
-
}
|
|
962
|
-
else if (position.character_colors?.length) {
|
|
963
|
-
ctx.font = `${textFontSize}px ${position.font}`;
|
|
964
|
-
const textHeight = wrapTextMultiColor(ctx, position.text, position.character_colors, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
965
|
-
currentY += textHeight;
|
|
966
|
-
drawnHeight += textHeight;
|
|
967
|
-
}
|
|
968
967
|
else {
|
|
969
|
-
ctx.font = `${textFontSize}px ${position.font}`;
|
|
970
|
-
ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || LAYOUT.LABEL_COLOR;
|
|
971
968
|
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
972
969
|
currentY += textResult.height;
|
|
973
970
|
drawnHeight += textResult.height;
|
|
@@ -983,12 +980,26 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
983
980
|
drawnHeight += result.height;
|
|
984
981
|
}
|
|
985
982
|
if (showLabels.font && position.font) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
983
|
+
// Render "Font: " với font mặc định
|
|
984
|
+
const prefix = "Font: ";
|
|
985
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
986
|
+
const prefixWidth = ctx.measureText(prefix).width;
|
|
987
|
+
let currentX = x + prefixWidth;
|
|
988
|
+
ctx.fillText(prefix, x, currentY);
|
|
989
|
+
// Render tên font với font từ config
|
|
990
|
+
ctx.font = `${otherFontSize}px ${position.font}`;
|
|
991
|
+
const fontNameWidth = ctx.measureText(position.font).width;
|
|
992
|
+
ctx.fillText(position.font, currentX, currentY);
|
|
993
|
+
currentX += fontNameWidth;
|
|
994
|
+
// Render "(Mặc định)" hoặc "(Custom)" với font mặc định
|
|
995
|
+
const suffix = position.is_font_default === true ? " (Mặc định)" : " (Custom)";
|
|
996
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
997
|
+
ctx.measureText(suffix).width;
|
|
998
|
+
ctx.fillText(suffix, currentX, currentY);
|
|
999
|
+
// Tính toán height và di chuyển cursorY
|
|
1000
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1001
|
+
currentY += lineHeight;
|
|
1002
|
+
drawnHeight += lineHeight;
|
|
992
1003
|
}
|
|
993
1004
|
if (showLabels.color) {
|
|
994
1005
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1016,21 +1027,56 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
1016
1027
|
return drawnHeight;
|
|
1017
1028
|
};
|
|
1018
1029
|
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
1019
|
-
|
|
1030
|
+
// Dùng cùng font size với Text cho label và value icon
|
|
1031
|
+
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1020
1032
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
1021
1033
|
ctx.save();
|
|
1022
|
-
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1023
1034
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1024
1035
|
let cursorY = y;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1036
|
+
// Tách label "Icon:" (in đậm) và phần value (thường)
|
|
1037
|
+
const iconLabel = "Icon:";
|
|
1038
|
+
let iconValue;
|
|
1039
|
+
if (position.is_delete_icon) {
|
|
1040
|
+
// Ưu tiên hiển thị không có icon nếu được đánh dấu xóa
|
|
1041
|
+
iconValue = "(không có icon)";
|
|
1042
|
+
}
|
|
1043
|
+
else if (position.note) {
|
|
1044
|
+
iconValue = position.note;
|
|
1045
|
+
}
|
|
1046
|
+
else if (position.icon_name && position.icon_name.trim().length > 0) {
|
|
1047
|
+
// Nếu có icon_name thì hiển thị tên đó
|
|
1048
|
+
iconValue = position.icon_name;
|
|
1049
|
+
}
|
|
1050
|
+
else if (position.icon === 0) {
|
|
1051
|
+
// Icon mặc định theo file thêu
|
|
1052
|
+
iconValue = "(icon mặc định theo file thêu)";
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
// Fallback: hiển thị mã icon (ép sang string)
|
|
1056
|
+
iconValue = String(position.icon);
|
|
1057
|
+
}
|
|
1058
|
+
// Vẽ label Icon: (bold, màu label mặc định)
|
|
1059
|
+
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1060
|
+
ctx.fillText(iconLabel, x, cursorY);
|
|
1061
|
+
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1062
|
+
// Vẽ value kế bên, font thường, màu đỏ giống text value
|
|
1063
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1064
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1065
|
+
const valueText = ` ${iconValue}`;
|
|
1066
|
+
ctx.fillText(valueText, x + labelWidth, cursorY);
|
|
1067
|
+
// Reset lại màu về label color cho các phần text tiếp theo (màu chỉ, v.v.)
|
|
1068
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1069
|
+
const valueWidth = ctx.measureText(valueText).width;
|
|
1070
|
+
const iconResult = {
|
|
1071
|
+
height: iconFontSize + lineGap,
|
|
1072
|
+
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1073
|
+
lastLineWidth: labelWidth + valueWidth,
|
|
1074
|
+
lastLineY: cursorY,
|
|
1075
|
+
};
|
|
1030
1076
|
// Draw icon image
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const img = imageRefs.current.get(
|
|
1077
|
+
const iconUrl = getIconImageUrl(position);
|
|
1078
|
+
if (iconUrl) {
|
|
1079
|
+
const img = imageRefs.current.get(iconUrl);
|
|
1034
1080
|
if (img?.complete && img.naturalHeight > 0) {
|
|
1035
1081
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1036
1082
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
@@ -1043,11 +1089,14 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1043
1089
|
}
|
|
1044
1090
|
cursorY += iconResult.height;
|
|
1045
1091
|
// Draw color swatches (prefer layer_colors, fallback to single color)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1092
|
+
// Nếu icon đã bị xóa thì không cần hiển thị màu chỉ nữa
|
|
1093
|
+
const iconColors = position.is_delete_icon
|
|
1094
|
+
? null
|
|
1095
|
+
: position.layer_colors?.length
|
|
1096
|
+
? position.layer_colors
|
|
1097
|
+
: position.color
|
|
1098
|
+
? [position.color]
|
|
1099
|
+
: null;
|
|
1051
1100
|
const layerCount = position.layer_colors?.length ?? 0;
|
|
1052
1101
|
const hasMultiLayerColors = layerCount > 1;
|
|
1053
1102
|
const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;
|