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.
@@ -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;AAoWD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAqHvD,CAAC;AA27BF,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"}
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: 160,
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: 40,
124
- PADDING: 40,
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: 300,
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
- if (position.icon !== 0) {
292
- const iconUrl = getImageUrl("icon", position.icon);
293
- if (!seen.has(iconUrl)) {
294
- entries.push({ url: iconUrl });
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
- const words = text.split(" ").filter((word) => word.length > 0);
370
- if (words.length === 0)
371
- return [""];
372
- const lines = [];
373
- let currentLine = words[0];
374
- for (let i = 1; i < words.length; i++) {
375
- const testLine = `${currentLine} ${words[i]}`;
376
- if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
377
- lines.push(currentLine);
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
- lines.push(currentLine);
407
- let currentY = y;
408
- lines.forEach((line, lineIdx) => {
409
- let currentX = x;
410
- const startCharIdx = lineIdx > 0 ? lineStartIndices[lineIdx] : 0;
411
- for (let i = 0; i < line.length; i++) {
412
- const char = line[i];
413
- const globalCharIdx = startCharIdx + i;
414
- const colorIndex = globalCharIdx % colors.length;
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
- currentY += lineHeight;
385
+ result.push(currentLine);
421
386
  });
422
- return lines.length * lineHeight;
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
- if (position.icon !== 0) {
487
- loadImage(getImageUrl("icon", position.icon), imageRefs, incrementCounter);
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 height
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
- // Calculate currentY: padding top + actual warning height (no spacing)
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 = `Note: ${message.trim()}`;
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 = DEFAULT_WARNING_COLOR;
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(900, height);
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
- const fontLabel = allDefault ? `Font: ${values.font} (Mặc định)` : `Font: ${values.font} (Custom)`;
896
- const result = wrapText(ctx, fontLabel, x, cursorY, maxWidth, fontSize + lineGap);
897
- cursorY += result.height;
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
- const fontLabel = position.is_font_default === true
987
- ? `Font: ${position.font} (Mặc định)`
988
- : `Font: ${position.font} (Custom)`;
989
- const result = wrapText(ctx, fontLabel, x, currentY, maxWidth, otherFontSize + lineGap);
990
- currentY += result.height;
991
- drawnHeight += result.height;
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
- const iconFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
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
- const iconText = position.note ? `Icon: ${position.note}` :
1026
- position.icon === 0
1027
- ? `Icon: (icon mặc định theo file thêu)`
1028
- : `Icon: ${position.icon}`;
1029
- const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
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 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
- if (position.icon !== 0) {
1032
- const url = getImageUrl("icon", position.icon);
1033
- const img = imageRefs.current.get(url);
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
- const iconColors = position.layer_colors?.length
1047
- ? position.layer_colors
1048
- : position.color
1049
- ? [position.color]
1050
- : null;
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;