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.
- package/dist/components/EmbroideryQCImage.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +193 -56
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +193 -56
- 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
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ const LAYOUT = {
|
|
|
48
48
|
// Font sizes (base values, will be multiplied by scaleFactor)
|
|
49
49
|
HEADER_FONT_SIZE: 220,
|
|
50
50
|
TEXT_FONT_SIZE: 200,
|
|
51
|
-
OTHER_FONT_SIZE:
|
|
51
|
+
OTHER_FONT_SIZE: 180,
|
|
52
52
|
// Colors
|
|
53
53
|
HEADER_COLOR: "#000000",
|
|
54
54
|
LABEL_COLOR: "#444444",
|
|
@@ -57,12 +57,12 @@ const LAYOUT = {
|
|
|
57
57
|
TEXT_ALIGN: "left",
|
|
58
58
|
TEXT_BASELINE: "top",
|
|
59
59
|
// Spacing
|
|
60
|
-
LINE_GAP:
|
|
61
|
-
PADDING:
|
|
60
|
+
LINE_GAP: 50,
|
|
61
|
+
PADDING: 50,
|
|
62
62
|
SECTION_SPACING: 60,
|
|
63
63
|
ELEMENT_SPACING: 100,
|
|
64
64
|
SWATCH_SPACING: 25,
|
|
65
|
-
FLORAL_SPACING:
|
|
65
|
+
FLORAL_SPACING: 100,
|
|
66
66
|
// Visual styling
|
|
67
67
|
SWATCH_HEIGHT_RATIO: 2.025,
|
|
68
68
|
UNDERLINE_POSITION: 0.9,
|
|
@@ -105,6 +105,17 @@ const getImageUrl = (type, value) => {
|
|
|
105
105
|
return `${BASE_URLS.THREAD_COLOR}/${value}.webp`;
|
|
106
106
|
};
|
|
107
107
|
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
108
|
+
const getIconImageUrl = (position) => {
|
|
109
|
+
if (position.is_delete_icon)
|
|
110
|
+
return null;
|
|
111
|
+
if (position.icon_image && position.icon_image.trim().length > 0) {
|
|
112
|
+
return position.icon_image;
|
|
113
|
+
}
|
|
114
|
+
if (position.icon !== 0) {
|
|
115
|
+
return getImageUrl("icon", position.icon);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
};
|
|
108
119
|
const ensureImage = (existing) => {
|
|
109
120
|
if (existing && existing.crossOrigin === "anonymous") {
|
|
110
121
|
return existing;
|
|
@@ -136,14 +147,14 @@ const loadImage = (url, imageRefs, onLoad) => {
|
|
|
136
147
|
img.onerror = () => {
|
|
137
148
|
if (!attemptedProxy) {
|
|
138
149
|
attemptedProxy = true;
|
|
139
|
-
img.src = getProxyUrl(url);
|
|
150
|
+
img.src = getProxyUrl(getResizeUrl(url));
|
|
140
151
|
return;
|
|
141
152
|
}
|
|
142
153
|
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
143
154
|
cleanup();
|
|
144
155
|
onLoad();
|
|
145
156
|
};
|
|
146
|
-
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
157
|
+
img.src = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
147
158
|
};
|
|
148
159
|
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
149
160
|
const key = cacheKey ?? url;
|
|
@@ -181,13 +192,13 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
181
192
|
target.onerror = () => {
|
|
182
193
|
if (!attemptedProxy) {
|
|
183
194
|
attemptedProxy = true;
|
|
184
|
-
target.src = getProxyUrl(url);
|
|
195
|
+
target.src = getProxyUrl(getResizeUrl(url));
|
|
185
196
|
return;
|
|
186
197
|
}
|
|
187
198
|
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
188
199
|
finalize();
|
|
189
200
|
};
|
|
190
|
-
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
201
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
191
202
|
if (target.src !== desiredSrc) {
|
|
192
203
|
target.src = desiredSrc;
|
|
193
204
|
}
|
|
@@ -196,6 +207,57 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
196
207
|
}
|
|
197
208
|
});
|
|
198
209
|
};
|
|
210
|
+
const getResizeUrl = (url) => {
|
|
211
|
+
try {
|
|
212
|
+
const urlObj = new URL(url);
|
|
213
|
+
// Xử lý cdn.shopify.com
|
|
214
|
+
if (urlObj.hostname === 'cdn.shopify.com') {
|
|
215
|
+
// Set hoặc update query param width=400
|
|
216
|
+
urlObj.searchParams.set('width', '400');
|
|
217
|
+
return urlObj.toString();
|
|
218
|
+
}
|
|
219
|
+
// Xử lý m.media-amazon.com
|
|
220
|
+
if (urlObj.hostname === 'm.media-amazon.com') {
|
|
221
|
+
const pathname = urlObj.pathname;
|
|
222
|
+
// Split pathname theo dấu /
|
|
223
|
+
const pathArr = pathname.split('/');
|
|
224
|
+
// Lấy filename (phần cuối cùng)
|
|
225
|
+
const filename = pathArr[pathArr.length - 1];
|
|
226
|
+
// Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
|
|
227
|
+
const cleanedFilename = filename.replace(/\._.*_/g, '');
|
|
228
|
+
// Split filename đã clean theo dấu .
|
|
229
|
+
const parts = cleanedFilename.split('.');
|
|
230
|
+
if (parts.length >= 2) {
|
|
231
|
+
// Lấy phần đầu và phần cuối
|
|
232
|
+
const firstPart = parts[0];
|
|
233
|
+
const lastPart = parts[parts.length - 1];
|
|
234
|
+
// Chèn _AC_SX400_ vào giữa và join lại
|
|
235
|
+
const newFilename = `${firstPart}._AC_SX400_.${lastPart}`;
|
|
236
|
+
// Thay filename mới vào pathArr
|
|
237
|
+
pathArr[pathArr.length - 1] = newFilename;
|
|
238
|
+
// Join lại
|
|
239
|
+
urlObj.pathname = pathArr.join('/');
|
|
240
|
+
return urlObj.toString();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Xử lý i.etsystatic.com
|
|
244
|
+
if (urlObj.hostname === 'i.etsystatic.com') {
|
|
245
|
+
const pathname = urlObj.pathname;
|
|
246
|
+
// Thay il_fullxfull bằng il_400x400
|
|
247
|
+
if (pathname.includes('il_fullxfull')) {
|
|
248
|
+
const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
|
|
249
|
+
urlObj.pathname = newPathname;
|
|
250
|
+
return urlObj.toString();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Nếu không phải các domain cần xử lý, return URL gốc
|
|
254
|
+
return url;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// Nếu URL không hợp lệ, return URL gốc
|
|
258
|
+
return url;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
199
261
|
const preloadFonts = async (config) => {
|
|
200
262
|
if (config.error_message || !config.sides?.length)
|
|
201
263
|
return;
|
|
@@ -225,12 +287,10 @@ const preloadImages = async (config, imageRefs) => {
|
|
|
225
287
|
config.sides.forEach((side) => {
|
|
226
288
|
side.positions.forEach((position) => {
|
|
227
289
|
if (position.type === "ICON") {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
seen.add(iconUrl);
|
|
233
|
-
}
|
|
290
|
+
const iconUrl = getIconImageUrl(position);
|
|
291
|
+
if (iconUrl && !seen.has(iconUrl)) {
|
|
292
|
+
entries.push({ url: iconUrl });
|
|
293
|
+
seen.add(iconUrl);
|
|
234
294
|
}
|
|
235
295
|
if (position.color) {
|
|
236
296
|
const threadUrl = getImageUrl("threadColor", position.color);
|
|
@@ -303,23 +363,30 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
303
363
|
};
|
|
304
364
|
};
|
|
305
365
|
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
currentLine = words[i];
|
|
366
|
+
// Mỗi '\n' tương đương với một line break giống như khi wrap tự động.
|
|
367
|
+
const segments = text.split("\n");
|
|
368
|
+
const result = [];
|
|
369
|
+
segments.forEach((segment) => {
|
|
370
|
+
const words = segment.split(" ").filter((word) => word.length > 0);
|
|
371
|
+
if (words.length === 0) {
|
|
372
|
+
// Nếu đoạn rỗng, thêm một dòng trống (break đúng 1 line)
|
|
373
|
+
result.push("");
|
|
374
|
+
return;
|
|
316
375
|
}
|
|
317
|
-
|
|
318
|
-
|
|
376
|
+
let currentLine = words[0];
|
|
377
|
+
for (let i = 1; i < words.length; i++) {
|
|
378
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
379
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
380
|
+
result.push(currentLine);
|
|
381
|
+
currentLine = words[i];
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
currentLine = testLine;
|
|
385
|
+
}
|
|
319
386
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return
|
|
387
|
+
result.push(currentLine);
|
|
388
|
+
});
|
|
389
|
+
return result.length ? result : [""];
|
|
323
390
|
};
|
|
324
391
|
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
325
392
|
let swatchX = startX;
|
|
@@ -383,8 +450,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
383
450
|
config.sides.forEach((side) => {
|
|
384
451
|
side.positions.forEach((position) => {
|
|
385
452
|
if (position.type === "ICON") {
|
|
386
|
-
|
|
387
|
-
|
|
453
|
+
const iconUrl = getIconImageUrl(position);
|
|
454
|
+
if (iconUrl) {
|
|
455
|
+
loadImage(iconUrl, imageRefs, incrementCounter);
|
|
388
456
|
}
|
|
389
457
|
position.layer_colors?.forEach((color) => {
|
|
390
458
|
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
@@ -435,7 +503,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
435
503
|
return;
|
|
436
504
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
437
505
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
438
|
-
// Calculate warning height (with scaleFactor = 1 for measurement)
|
|
506
|
+
// Calculate warning & message height (with scaleFactor = 1 for measurement)
|
|
439
507
|
let warningHeight = 0;
|
|
440
508
|
let warningLineHeight = 0;
|
|
441
509
|
let warningLineCount = 0;
|
|
@@ -454,6 +522,24 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
454
522
|
warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
|
|
455
523
|
}
|
|
456
524
|
}
|
|
525
|
+
let messageHeight = 0;
|
|
526
|
+
let messageLineHeight = 0;
|
|
527
|
+
let messageLineCount = 0;
|
|
528
|
+
if (config.message) {
|
|
529
|
+
const measureMessageCanvas = document.createElement("canvas");
|
|
530
|
+
measureMessageCanvas.width = canvas.width;
|
|
531
|
+
measureMessageCanvas.height = canvas.height;
|
|
532
|
+
const measureMessageCtx = measureMessageCanvas.getContext("2d");
|
|
533
|
+
if (measureMessageCtx) {
|
|
534
|
+
measureMessageCtx.textAlign = "left";
|
|
535
|
+
measureMessageCtx.textBaseline = "top";
|
|
536
|
+
measureMessageCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
|
|
537
|
+
const messageLines = buildWrappedLines(measureMessageCtx, config.message.trim(), canvas.width - LAYOUT.PADDING * 4);
|
|
538
|
+
messageLineCount = messageLines.length;
|
|
539
|
+
messageLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
|
|
540
|
+
messageHeight = messageLineCount * messageLineHeight + LAYOUT.PADDING;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
457
543
|
if (config.image_url) {
|
|
458
544
|
const mockupImage = imageRefs.current.get(config.image_url);
|
|
459
545
|
if (mockupImage) {
|
|
@@ -486,27 +572,40 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
486
572
|
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
487
573
|
let measureY = LAYOUT.PADDING;
|
|
488
574
|
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
489
|
-
// Add warning text height (without bottom padding, no spacing)
|
|
575
|
+
// Add warning & message text height (without bottom padding, no spacing)
|
|
490
576
|
if (config.warning_message) {
|
|
491
577
|
const warningTextHeight = warningHeight - LAYOUT.PADDING;
|
|
492
578
|
measureY += warningTextHeight;
|
|
493
579
|
}
|
|
580
|
+
if (config.message) {
|
|
581
|
+
const messageTextHeight = messageHeight - LAYOUT.PADDING;
|
|
582
|
+
measureY += messageTextHeight;
|
|
583
|
+
}
|
|
494
584
|
config.sides.forEach((side) => {
|
|
495
585
|
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
496
586
|
measureY += sideHeight + measureSpacing;
|
|
497
587
|
});
|
|
498
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight) / measureY));
|
|
588
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
|
|
499
589
|
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
500
|
-
// Render warning with scaleFactor and get actual
|
|
590
|
+
// Render warning & message with scaleFactor and get actual heights
|
|
501
591
|
let actualWarningHeight = 0;
|
|
592
|
+
let actualMessageHeight = 0;
|
|
502
593
|
if (config.warning_message) {
|
|
503
594
|
actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
|
|
504
595
|
}
|
|
505
|
-
|
|
596
|
+
if (config.message) {
|
|
597
|
+
actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, actualWarningHeight, "", // message: không cần prefix "Note"
|
|
598
|
+
DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
506
602
|
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
507
603
|
if (config.warning_message && actualWarningHeight > 0) {
|
|
508
604
|
currentY += actualWarningHeight;
|
|
509
605
|
}
|
|
606
|
+
if (config.message && actualMessageHeight > 0) {
|
|
607
|
+
currentY += actualMessageHeight;
|
|
608
|
+
}
|
|
510
609
|
config.sides.forEach((side) => {
|
|
511
610
|
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
512
611
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
@@ -548,8 +647,8 @@ const renderErrorState = (ctx, canvas, message) => {
|
|
|
548
647
|
});
|
|
549
648
|
ctx.restore();
|
|
550
649
|
};
|
|
551
|
-
const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
552
|
-
const sanitizedMessage =
|
|
650
|
+
const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefix = "Note: ", color = DEFAULT_WARNING_COLOR) => {
|
|
651
|
+
const sanitizedMessage = `${prefix}${message.trim()}`;
|
|
553
652
|
const horizontalPadding = LAYOUT.PADDING * 2 * scaleFactor;
|
|
554
653
|
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
555
654
|
const baseFontSize = LAYOUT.HEADER_FONT_SIZE * 0.7 * scaleFactor;
|
|
@@ -558,7 +657,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
558
657
|
ctx.save();
|
|
559
658
|
ctx.textAlign = "left";
|
|
560
659
|
ctx.textBaseline = "top";
|
|
561
|
-
ctx.fillStyle =
|
|
660
|
+
ctx.fillStyle = color;
|
|
562
661
|
ctx.font = `${baseFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
563
662
|
let fontSize = baseFontSize;
|
|
564
663
|
let lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -577,7 +676,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
577
676
|
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
578
677
|
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
579
678
|
}
|
|
580
|
-
const startY = LAYOUT.PADDING * scaleFactor;
|
|
679
|
+
const startY = LAYOUT.PADDING * scaleFactor + offsetY;
|
|
581
680
|
lines.forEach((line, index) => {
|
|
582
681
|
const y = startY + index * lineHeight;
|
|
583
682
|
ctx.fillText(line, leftX, y);
|
|
@@ -601,7 +700,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
|
601
700
|
ctx.drawImage(mockupImg, x, y, width, height);
|
|
602
701
|
// Draw florals
|
|
603
702
|
if (floralAssets.length > 0) {
|
|
604
|
-
const floralH = Math.min(
|
|
703
|
+
const floralH = Math.min(500, height);
|
|
605
704
|
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
606
705
|
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
607
706
|
const img = floralAssets[i];
|
|
@@ -930,21 +1029,56 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
930
1029
|
return drawnHeight;
|
|
931
1030
|
};
|
|
932
1031
|
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
933
|
-
|
|
1032
|
+
// Dùng cùng font size với Text cho label và value icon
|
|
1033
|
+
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
934
1034
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
935
1035
|
ctx.save();
|
|
936
|
-
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
937
1036
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
938
1037
|
let cursorY = y;
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1038
|
+
// Tách label "Icon:" (in đậm) và phần value (thường)
|
|
1039
|
+
const iconLabel = "Icon:";
|
|
1040
|
+
let iconValue;
|
|
1041
|
+
if (position.is_delete_icon) {
|
|
1042
|
+
// Ưu tiên hiển thị không có icon nếu được đánh dấu xóa
|
|
1043
|
+
iconValue = "(không có icon)";
|
|
1044
|
+
}
|
|
1045
|
+
else if (position.note) {
|
|
1046
|
+
iconValue = position.note;
|
|
1047
|
+
}
|
|
1048
|
+
else if (position.icon_name && position.icon_name.trim().length > 0) {
|
|
1049
|
+
// Nếu có icon_name thì hiển thị tên đó
|
|
1050
|
+
iconValue = position.icon_name;
|
|
1051
|
+
}
|
|
1052
|
+
else if (position.icon === 0) {
|
|
1053
|
+
// Icon mặc định theo file thêu
|
|
1054
|
+
iconValue = "(icon mặc định theo file thêu)";
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
// Fallback: hiển thị mã icon (ép sang string)
|
|
1058
|
+
iconValue = String(position.icon);
|
|
1059
|
+
}
|
|
1060
|
+
// Vẽ label Icon: (bold, màu label mặc định)
|
|
1061
|
+
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1062
|
+
ctx.fillText(iconLabel, x, cursorY);
|
|
1063
|
+
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1064
|
+
// Vẽ value kế bên, font thường, màu đỏ giống text value
|
|
1065
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1066
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1067
|
+
const valueText = ` ${iconValue}`;
|
|
1068
|
+
ctx.fillText(valueText, x + labelWidth, cursorY);
|
|
1069
|
+
// Reset lại màu về label color cho các phần text tiếp theo (màu chỉ, v.v.)
|
|
1070
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1071
|
+
const valueWidth = ctx.measureText(valueText).width;
|
|
1072
|
+
const iconResult = {
|
|
1073
|
+
height: iconFontSize + lineGap,
|
|
1074
|
+
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1075
|
+
lastLineWidth: labelWidth + valueWidth,
|
|
1076
|
+
lastLineY: cursorY,
|
|
1077
|
+
};
|
|
944
1078
|
// Draw icon image
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const img = imageRefs.current.get(
|
|
1079
|
+
const iconUrl = getIconImageUrl(position);
|
|
1080
|
+
if (iconUrl) {
|
|
1081
|
+
const img = imageRefs.current.get(iconUrl);
|
|
948
1082
|
if (img?.complete && img.naturalHeight > 0) {
|
|
949
1083
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
950
1084
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
@@ -957,11 +1091,14 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
957
1091
|
}
|
|
958
1092
|
cursorY += iconResult.height;
|
|
959
1093
|
// Draw color swatches (prefer layer_colors, fallback to single color)
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1094
|
+
// Nếu icon đã bị xóa thì không cần hiển thị màu chỉ nữa
|
|
1095
|
+
const iconColors = position.is_delete_icon
|
|
1096
|
+
? null
|
|
1097
|
+
: position.layer_colors?.length
|
|
1098
|
+
? position.layer_colors
|
|
1099
|
+
: position.color
|
|
1100
|
+
? [position.color]
|
|
1101
|
+
: null;
|
|
965
1102
|
const layerCount = position.layer_colors?.length ?? 0;
|
|
966
1103
|
const hasMultiLayerColors = layerCount > 1;
|
|
967
1104
|
const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;
|