embroidery-qc-image 1.0.24 → 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;AAm7BF,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
@@ -46,7 +46,7 @@ const LAYOUT = {
46
46
  // Font sizes (base values, will be multiplied by scaleFactor)
47
47
  HEADER_FONT_SIZE: 220,
48
48
  TEXT_FONT_SIZE: 200,
49
- OTHER_FONT_SIZE: 160,
49
+ OTHER_FONT_SIZE: 180,
50
50
  // Colors
51
51
  HEADER_COLOR: "#000000",
52
52
  LABEL_COLOR: "#444444",
@@ -55,12 +55,12 @@ const LAYOUT = {
55
55
  TEXT_ALIGN: "left",
56
56
  TEXT_BASELINE: "top",
57
57
  // Spacing
58
- LINE_GAP: 40,
59
- PADDING: 40,
58
+ LINE_GAP: 50,
59
+ PADDING: 50,
60
60
  SECTION_SPACING: 60,
61
61
  ELEMENT_SPACING: 100,
62
62
  SWATCH_SPACING: 25,
63
- FLORAL_SPACING: 300,
63
+ FLORAL_SPACING: 100,
64
64
  // Visual styling
65
65
  SWATCH_HEIGHT_RATIO: 2.025,
66
66
  UNDERLINE_POSITION: 0.9,
@@ -103,6 +103,17 @@ const getImageUrl = (type, value) => {
103
103
  return `${BASE_URLS.THREAD_COLOR}/${value}.webp`;
104
104
  };
105
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
+ };
106
117
  const ensureImage = (existing) => {
107
118
  if (existing && existing.crossOrigin === "anonymous") {
108
119
  return existing;
@@ -134,14 +145,14 @@ const loadImage = (url, imageRefs, onLoad) => {
134
145
  img.onerror = () => {
135
146
  if (!attemptedProxy) {
136
147
  attemptedProxy = true;
137
- img.src = getProxyUrl(url);
148
+ img.src = getProxyUrl(getResizeUrl(url));
138
149
  return;
139
150
  }
140
151
  img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
141
152
  cleanup();
142
153
  onLoad();
143
154
  };
144
- img.src = attemptedProxy ? getProxyUrl(url) : url;
155
+ img.src = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
145
156
  };
146
157
  const loadImageAsync = (url, imageRefs, cacheKey) => {
147
158
  const key = cacheKey ?? url;
@@ -179,13 +190,13 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
179
190
  target.onerror = () => {
180
191
  if (!attemptedProxy) {
181
192
  attemptedProxy = true;
182
- target.src = getProxyUrl(url);
193
+ target.src = getProxyUrl(getResizeUrl(url));
183
194
  return;
184
195
  }
185
196
  target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
186
197
  finalize();
187
198
  };
188
- const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
199
+ const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
189
200
  if (target.src !== desiredSrc) {
190
201
  target.src = desiredSrc;
191
202
  }
@@ -194,6 +205,57 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
194
205
  }
195
206
  });
196
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
+ };
197
259
  const preloadFonts = async (config) => {
198
260
  if (config.error_message || !config.sides?.length)
199
261
  return;
@@ -223,12 +285,10 @@ const preloadImages = async (config, imageRefs) => {
223
285
  config.sides.forEach((side) => {
224
286
  side.positions.forEach((position) => {
225
287
  if (position.type === "ICON") {
226
- if (position.icon !== 0) {
227
- const iconUrl = getImageUrl("icon", position.icon);
228
- if (!seen.has(iconUrl)) {
229
- entries.push({ url: iconUrl });
230
- seen.add(iconUrl);
231
- }
288
+ const iconUrl = getIconImageUrl(position);
289
+ if (iconUrl && !seen.has(iconUrl)) {
290
+ entries.push({ url: iconUrl });
291
+ seen.add(iconUrl);
232
292
  }
233
293
  if (position.color) {
234
294
  const threadUrl = getImageUrl("threadColor", position.color);
@@ -301,23 +361,30 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
301
361
  };
302
362
  };
303
363
  const buildWrappedLines = (ctx, text, maxWidth) => {
304
- const words = text.split(" ").filter((word) => word.length > 0);
305
- if (words.length === 0)
306
- return [""];
307
- const lines = [];
308
- let currentLine = words[0];
309
- for (let i = 1; i < words.length; i++) {
310
- const testLine = `${currentLine} ${words[i]}`;
311
- if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
312
- lines.push(currentLine);
313
- currentLine = words[i];
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;
314
373
  }
315
- else {
316
- currentLine = testLine;
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
+ }
317
384
  }
318
- }
319
- lines.push(currentLine);
320
- return lines;
385
+ result.push(currentLine);
386
+ });
387
+ return result.length ? result : [""];
321
388
  };
322
389
  const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
323
390
  let swatchX = startX;
@@ -381,8 +448,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
381
448
  config.sides.forEach((side) => {
382
449
  side.positions.forEach((position) => {
383
450
  if (position.type === "ICON") {
384
- if (position.icon !== 0) {
385
- loadImage(getImageUrl("icon", position.icon), imageRefs, incrementCounter);
451
+ const iconUrl = getIconImageUrl(position);
452
+ if (iconUrl) {
453
+ loadImage(iconUrl, imageRefs, incrementCounter);
386
454
  }
387
455
  position.layer_colors?.forEach((color) => {
388
456
  loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
@@ -433,7 +501,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
433
501
  return;
434
502
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
435
503
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
436
- // Calculate warning height (with scaleFactor = 1 for measurement)
504
+ // Calculate warning & message height (with scaleFactor = 1 for measurement)
437
505
  let warningHeight = 0;
438
506
  let warningLineHeight = 0;
439
507
  let warningLineCount = 0;
@@ -452,6 +520,24 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
452
520
  warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
453
521
  }
454
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
+ }
455
541
  if (config.image_url) {
456
542
  const mockupImage = imageRefs.current.get(config.image_url);
457
543
  if (mockupImage) {
@@ -484,27 +570,40 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
484
570
  measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
485
571
  let measureY = LAYOUT.PADDING;
486
572
  const measureSpacing = LAYOUT.ELEMENT_SPACING;
487
- // Add warning text height (without bottom padding, no spacing)
573
+ // Add warning & message text height (without bottom padding, no spacing)
488
574
  if (config.warning_message) {
489
575
  const warningTextHeight = warningHeight - LAYOUT.PADDING;
490
576
  measureY += warningTextHeight;
491
577
  }
578
+ if (config.message) {
579
+ const messageTextHeight = messageHeight - LAYOUT.PADDING;
580
+ measureY += messageTextHeight;
581
+ }
492
582
  config.sides.forEach((side) => {
493
583
  const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
494
584
  measureY += sideHeight + measureSpacing;
495
585
  });
496
- 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));
497
587
  drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
498
- // Render warning with scaleFactor and get actual height
588
+ // Render warning & message with scaleFactor and get actual heights
499
589
  let actualWarningHeight = 0;
590
+ let actualMessageHeight = 0;
500
591
  if (config.warning_message) {
501
592
  actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
502
593
  }
503
- // 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)
504
600
  let currentY = LAYOUT.PADDING * scaleFactor;
505
601
  if (config.warning_message && actualWarningHeight > 0) {
506
602
  currentY += actualWarningHeight;
507
603
  }
604
+ if (config.message && actualMessageHeight > 0) {
605
+ currentY += actualMessageHeight;
606
+ }
508
607
  config.sides.forEach((side) => {
509
608
  const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
510
609
  currentY += sideHeight + measureSpacing * scaleFactor;
@@ -546,8 +645,8 @@ const renderErrorState = (ctx, canvas, message) => {
546
645
  });
547
646
  ctx.restore();
548
647
  };
549
- const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
550
- 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()}`;
551
650
  const horizontalPadding = LAYOUT.PADDING * 2 * scaleFactor;
552
651
  const maxWidth = canvas.width - horizontalPadding * 2;
553
652
  const baseFontSize = LAYOUT.HEADER_FONT_SIZE * 0.7 * scaleFactor;
@@ -556,7 +655,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
556
655
  ctx.save();
557
656
  ctx.textAlign = "left";
558
657
  ctx.textBaseline = "top";
559
- ctx.fillStyle = DEFAULT_WARNING_COLOR;
658
+ ctx.fillStyle = color;
560
659
  ctx.font = `${baseFontSize}px ${LAYOUT.FONT_FAMILY}`;
561
660
  let fontSize = baseFontSize;
562
661
  let lineGap = LAYOUT.LINE_GAP * scaleFactor;
@@ -575,7 +674,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
575
674
  lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
576
675
  longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
577
676
  }
578
- const startY = LAYOUT.PADDING * scaleFactor;
677
+ const startY = LAYOUT.PADDING * scaleFactor + offsetY;
579
678
  lines.forEach((line, index) => {
580
679
  const y = startY + index * lineHeight;
581
680
  ctx.fillText(line, leftX, y);
@@ -599,7 +698,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
599
698
  ctx.drawImage(mockupImg, x, y, width, height);
600
699
  // Draw florals
601
700
  if (floralAssets.length > 0) {
602
- const floralH = Math.min(900, height);
701
+ const floralH = Math.min(500, height);
603
702
  let currentX = x - LAYOUT.FLORAL_SPACING;
604
703
  for (let i = floralAssets.length - 1; i >= 0; i--) {
605
704
  const img = floralAssets[i];
@@ -928,21 +1027,56 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
928
1027
  return drawnHeight;
929
1028
  };
930
1029
  const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
931
- 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;
932
1032
  const lineGap = LAYOUT.LINE_GAP * scaleFactor;
933
1033
  ctx.save();
934
- ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
935
1034
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
936
1035
  let cursorY = y;
937
- const iconText = position.note ? `Icon: ${position.note}` :
938
- position.icon === 0
939
- ? `Icon: (icon mặc định theo file thêu)`
940
- : `Icon: ${position.icon}`;
941
- 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
+ };
942
1076
  // Draw icon image
943
- if (position.icon !== 0) {
944
- const url = getImageUrl("icon", position.icon);
945
- const img = imageRefs.current.get(url);
1077
+ const iconUrl = getIconImageUrl(position);
1078
+ if (iconUrl) {
1079
+ const img = imageRefs.current.get(iconUrl);
946
1080
  if (img?.complete && img.naturalHeight > 0) {
947
1081
  const ratio = img.naturalWidth / img.naturalHeight;
948
1082
  const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
@@ -955,11 +1089,14 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
955
1089
  }
956
1090
  cursorY += iconResult.height;
957
1091
  // Draw color swatches (prefer layer_colors, fallback to single color)
958
- const iconColors = position.layer_colors?.length
959
- ? position.layer_colors
960
- : position.color
961
- ? [position.color]
962
- : 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;
963
1100
  const layerCount = position.layer_colors?.length ?? 0;
964
1101
  const hasMultiLayerColors = layerCount > 1;
965
1102
  const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;