embroidery-qc-image 1.0.28 → 1.0.30

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;AAofD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAy9CF,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;AAofD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AA2kDF,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.esm.js CHANGED
@@ -569,21 +569,21 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
569
569
  // Tìm scaleFactor tối ưu bằng binary search trong [0, 1]
570
570
  const maxIterations = 12;
571
571
  const epsilon = 0.001; // độ chính xác cho khoảng cách low-high
572
- const contentHeight = canvas.height - LAYOUT.PADDING;
572
+ const contentHeight = canvas.height;
573
573
  let low = 0;
574
574
  let high = 1;
575
575
  for (let i = 0; i < maxIterations; i++) {
576
576
  const testScale = (low + high) / 2;
577
577
  // (Không cần clear measureCanvas vì chỉ dùng để đo chiều cao, nhưng làm sạch cho dễ debug)
578
578
  measureCtx.clearRect(0, 0, measureCanvas.width, measureCanvas.height);
579
- // Đo warning & message đúng theo renderWarning, không cộng padding ở đây
580
- let testMeasureY = 0;
579
+ // Đo warning & message đúng theo renderWarning, bao gồm padding
580
+ let testMeasureY = LAYOUT.PADDING;
581
581
  if (config.warning_message) {
582
- const testWarningHeight = renderWarning(measureCtx, measureCanvas, config.warning_message, testScale);
582
+ const testWarningHeight = renderWarning(measureCtx, measureCanvas, config.warning_message, testScale, testMeasureY);
583
583
  testMeasureY += testWarningHeight;
584
584
  }
585
585
  if (config.message) {
586
- const testMessageHeight = renderWarning(measureCtx, measureCanvas, config.message, testScale, 0, "", DEFAULT_ERROR_COLOR);
586
+ const testMessageHeight = renderWarning(measureCtx, measureCanvas, config.message, testScale, testMeasureY, "", DEFAULT_ERROR_COLOR);
587
587
  testMeasureY += testMessageHeight;
588
588
  }
589
589
  // Đo lại chiều cao của các sides với scaleFactor, tiếp nối sau warning/message
@@ -591,7 +591,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
591
591
  const sideHeight = renderSide(measureCtx, side, testMeasureY, canvas.width, canvas.height, testScale, imageRefs, mockupBounds);
592
592
  testMeasureY += sideHeight + measureSpacing * testScale;
593
593
  });
594
- // Tổng chiều cao content (không gồm padding)
594
+ // Tổng chiều cao content
595
595
  const totalHeight = testMeasureY;
596
596
  if (totalHeight > contentHeight) {
597
597
  // Content đang cao hơn vùng cho phép -> giảm scale
@@ -607,23 +607,17 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
607
607
  }
608
608
  const scaleFactor = low;
609
609
  drawMockup(ctx, canvas, imageRefs);
610
+ // Calculate currentY: padding top + actual warning & message height (no spacing)
611
+ let currentY = LAYOUT.PADDING;
610
612
  // Render warning & message with scaleFactor and get actual heights
611
- let actualWarningHeight = 0;
612
- let actualMessageHeight = 0;
613
613
  if (config.warning_message) {
614
- actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
614
+ const actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor, currentY);
615
+ currentY += actualWarningHeight;
615
616
  }
616
617
  if (config.message) {
617
- actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, actualWarningHeight, "", // message: không cần prefix "Note"
618
+ const actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, currentY, "", // message: không cần prefix "Note"
618
619
  DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
619
620
  );
620
- }
621
- // Calculate currentY: padding top + actual warning & message height (no spacing)
622
- let currentY = LAYOUT.PADDING;
623
- if (config.warning_message && actualWarningHeight > 0) {
624
- currentY += actualWarningHeight;
625
- }
626
- if (config.message && actualMessageHeight > 0) {
627
621
  currentY += actualMessageHeight;
628
622
  }
629
623
  config.sides.forEach((side) => {
@@ -696,9 +690,8 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
696
690
  lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
697
691
  longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
698
692
  }
699
- const startY = LAYOUT.PADDING * scaleFactor + offsetY;
700
693
  lines.forEach((line, index) => {
701
- const y = startY + index * lineHeight;
694
+ const y = offsetY + index * lineHeight;
702
695
  ctx.fillText(line, leftX, y);
703
696
  });
704
697
  ctx.restore();
@@ -1167,25 +1160,123 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1167
1160
  currentY += result.height;
1168
1161
  }
1169
1162
  if (showLabels.font && position.font) {
1170
- // Render "Font: " với font mặc định
1171
1163
  const prefix = "Font: ";
1172
- ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
1173
- const prefixWidth = ctx.measureText(prefix).width;
1174
- let currentX = x + prefixWidth;
1175
- ctx.fillText(prefix, x, currentY);
1176
- // Render tên font với font từ config
1177
- ctx.font = `${otherFontSize}px ${position.font}`;
1178
- const fontNameWidth = ctx.measureText(position.font).width;
1179
- ctx.fillText(position.font, currentX, currentY);
1180
- currentX += fontNameWidth;
1181
- // Render "(Mặc định)" hoặc "(Custom)" với font mặc định
1182
1164
  const suffix = position.is_font_default === true ? " (Mặc định)" : " (Custom)";
1183
- ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
1184
- ctx.measureText(suffix).width;
1185
- ctx.fillText(suffix, currentX, currentY);
1186
- // Tính toán height và di chuyển cursorY
1165
+ const fontName = position.font;
1166
+ const fullText = `${prefix}${fontName}${suffix}`;
1187
1167
  const lineHeight = otherFontSize + lineGap;
1188
- currentY += lineHeight;
1168
+ const textTopY = currentY;
1169
+ const effectiveMaxWidth = mockupBounds
1170
+ ? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
1171
+ : maxWidth;
1172
+ const MIN_FONT_FONT_SIZE = otherFontSize * 0.5;
1173
+ const measureFontTextWidth = (fontSize) => {
1174
+ ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
1175
+ const prefixWidth = ctx.measureText(prefix).width;
1176
+ ctx.font = `${fontSize}px ${position.font}`;
1177
+ const fontNameWidth = ctx.measureText(fontName).width;
1178
+ ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
1179
+ const suffixWidth = ctx.measureText(suffix).width;
1180
+ return prefixWidth + fontNameWidth + suffixWidth;
1181
+ };
1182
+ const checkFontSizeFits = (fontSize) => {
1183
+ const textTopY = currentY;
1184
+ const lineHeight = fontSize + lineGap;
1185
+ const effectiveMaxWidth = mockupBounds
1186
+ ? getEffectiveMaxWidth(x, textTopY, lineHeight, maxWidth, mockupBounds)
1187
+ : maxWidth;
1188
+ const textWidth = measureFontTextWidth(fontSize);
1189
+ return textWidth <= effectiveMaxWidth;
1190
+ };
1191
+ let effectiveFontSize = otherFontSize;
1192
+ let needsWrap = false;
1193
+ // Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
1194
+ const baseMaxWidth = measureFontTextWidth(otherFontSize);
1195
+ if (baseMaxWidth > effectiveMaxWidth) {
1196
+ // Binary search để tìm font size lớn nhất mà vẫn vừa
1197
+ let left = MIN_FONT_FONT_SIZE;
1198
+ let right = otherFontSize;
1199
+ let bestFontSize = MIN_FONT_FONT_SIZE;
1200
+ while (right - left > 0.1) {
1201
+ const mid = (left + right) / 2;
1202
+ if (checkFontSizeFits(mid)) {
1203
+ bestFontSize = mid;
1204
+ left = mid;
1205
+ }
1206
+ else {
1207
+ right = mid;
1208
+ }
1209
+ }
1210
+ if (checkFontSizeFits(bestFontSize)) {
1211
+ // Bước 1 thành công: font size đã shrink vừa chiều ngang
1212
+ effectiveFontSize = Math.floor(bestFontSize);
1213
+ }
1214
+ else {
1215
+ // Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
1216
+ needsWrap = true;
1217
+ effectiveFontSize = otherFontSize;
1218
+ }
1219
+ }
1220
+ if (needsWrap) {
1221
+ // Bước 2: Xuống dòng với font size gốc
1222
+ ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
1223
+ const wrappedLines = buildWrappedLines(ctx, fullText, effectiveMaxWidth, x, textTopY, lineHeight, mockupBounds);
1224
+ wrappedLines.forEach((line, index) => {
1225
+ const lineY = textTopY + index * lineHeight;
1226
+ const lineEffectiveMaxWidth = mockupBounds
1227
+ ? getEffectiveMaxWidth(x, lineY, lineHeight, maxWidth, mockupBounds)
1228
+ : maxWidth;
1229
+ let lineToRender = line;
1230
+ const lineWidth = ctx.measureText(lineToRender).width;
1231
+ if (lineWidth > lineEffectiveMaxWidth) {
1232
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1233
+ lineToRender = lineToRender.slice(0, -1);
1234
+ }
1235
+ }
1236
+ ctx.fillText(lineToRender, x, lineY);
1237
+ });
1238
+ currentY += wrappedLines.length * lineHeight;
1239
+ }
1240
+ else {
1241
+ // Bước 1 thành công: Render với font size đã shrink (1 dòng)
1242
+ const shrunkTextTopY = currentY;
1243
+ const shrunkLineHeight = effectiveFontSize + lineGap;
1244
+ const shrunkEffectiveMaxWidth = mockupBounds
1245
+ ? getEffectiveMaxWidth(x, shrunkTextTopY, shrunkLineHeight, maxWidth, mockupBounds)
1246
+ : maxWidth;
1247
+ ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
1248
+ const prefixWidth = ctx.measureText(prefix).width;
1249
+ let currentX = x + prefixWidth;
1250
+ ctx.fillText(prefix, x, currentY);
1251
+ ctx.font = `${effectiveFontSize}px ${position.font}`;
1252
+ const fontNameWidth = ctx.measureText(fontName).width;
1253
+ const totalWidth = prefixWidth + fontNameWidth;
1254
+ if (totalWidth > shrunkEffectiveMaxWidth) {
1255
+ // Cần cắt font name
1256
+ let truncatedFontName = fontName;
1257
+ while (ctx.measureText(truncatedFontName).width > shrunkEffectiveMaxWidth - prefixWidth && truncatedFontName.length > 0) {
1258
+ truncatedFontName = truncatedFontName.slice(0, -1);
1259
+ }
1260
+ ctx.fillText(truncatedFontName, currentX, currentY);
1261
+ currentX += ctx.measureText(truncatedFontName).width;
1262
+ }
1263
+ else {
1264
+ ctx.fillText(fontName, currentX, currentY);
1265
+ currentX += fontNameWidth;
1266
+ }
1267
+ ctx.font = `${effectiveFontSize}px ${LAYOUT.FONT_FAMILY}`;
1268
+ const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
1269
+ if (remainingWidth > 0) {
1270
+ let truncatedSuffix = suffix;
1271
+ while (ctx.measureText(truncatedSuffix).width > remainingWidth && truncatedSuffix.length > 0) {
1272
+ truncatedSuffix = truncatedSuffix.slice(0, -1);
1273
+ }
1274
+ if (truncatedSuffix.length > 0) {
1275
+ ctx.fillText(truncatedSuffix, currentX, currentY);
1276
+ }
1277
+ }
1278
+ currentY += shrunkLineHeight;
1279
+ }
1189
1280
  }
1190
1281
  if (showLabels.color) {
1191
1282
  const colorValue = position.character_colors?.join(", ") || position.color;
@@ -1331,126 +1422,115 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1331
1422
  }
1332
1423
  }
1333
1424
  }
1334
- // Tính available width cho icon value (trừ đi khoảng trống cho icon_image)
1335
1425
  const availableWidth = Math.max(1, maxWidth - labelWidth - iconImageReservedWidth);
1336
- // Tính font-size hiệu dụng cho icon value
1337
- // Giới hạn thu nhỏ tối đa 50% (tối thiểu = iconFontSize * 0.5)
1426
+ const textCenterY = cursorY + iconImageHeight / 2;
1427
+ const valueStartX = x + labelWidth;
1428
+ const textTopY = textCenterY - iconFontSize / 2;
1429
+ const lineHeight = iconFontSize;
1430
+ const effectiveMaxWidth = mockupBounds
1431
+ ? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
1432
+ : availableWidth;
1338
1433
  const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
1339
1434
  const measureIconValueWidth = (fontSize) => {
1340
1435
  ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
1341
1436
  return ctx.measureText(` ${iconValue}`).width;
1342
1437
  };
1438
+ const checkFontSizeFits = (fontSize) => {
1439
+ const textTopY = textCenterY - fontSize / 2;
1440
+ const lineHeight = fontSize;
1441
+ const effectiveMaxWidth = mockupBounds
1442
+ ? getEffectiveMaxWidth(valueStartX, textTopY, lineHeight, availableWidth, mockupBounds)
1443
+ : availableWidth;
1444
+ const textWidth = measureIconValueWidth(fontSize);
1445
+ return textWidth <= effectiveMaxWidth;
1446
+ };
1343
1447
  let effectiveIconValueFontSize = iconFontSize;
1344
- const baseMaxWidth = measureIconValueWidth(iconFontSize);
1345
1448
  let needsWrap = false;
1346
- if (baseMaxWidth > availableWidth) {
1347
- const shrinkRatio = availableWidth / baseMaxWidth;
1348
- effectiveIconValueFontSize = Math.max(MIN_ICON_VALUE_FONT_SIZE, iconFontSize * shrinkRatio);
1349
- // Kiểm tra xem sau khi thu nhỏ đến 50% vẫn overflow không
1350
- const minMaxWidth = measureIconValueWidth(MIN_ICON_VALUE_FONT_SIZE);
1351
- if (minMaxWidth > availableWidth) {
1352
- // Vẫn overflow, cần dùng wrap text
1449
+ // Bước 1: Thử giảm font size, kiểm tra xem có vừa chiều ngang không
1450
+ const baseMaxWidth = measureIconValueWidth(iconFontSize);
1451
+ if (baseMaxWidth > effectiveMaxWidth) {
1452
+ // Binary search để tìm font size lớn nhất vẫn vừa
1453
+ let left = MIN_ICON_VALUE_FONT_SIZE;
1454
+ let right = iconFontSize;
1455
+ let bestFontSize = MIN_ICON_VALUE_FONT_SIZE;
1456
+ while (right - left > 0.1) {
1457
+ const mid = (left + right) / 2;
1458
+ if (checkFontSizeFits(mid)) {
1459
+ bestFontSize = mid;
1460
+ left = mid;
1461
+ }
1462
+ else {
1463
+ right = mid;
1464
+ }
1465
+ }
1466
+ if (checkFontSizeFits(bestFontSize)) {
1467
+ // Bước 1 thành công: font size đã shrink vừa chiều ngang
1468
+ effectiveIconValueFontSize = Math.floor(bestFontSize);
1469
+ }
1470
+ else {
1471
+ // Bước 1 thất bại: đã shrink đến MIN nhưng vẫn không vừa, sang bước 2
1353
1472
  needsWrap = true;
1354
- effectiveIconValueFontSize = MIN_ICON_VALUE_FONT_SIZE;
1473
+ effectiveIconValueFontSize = iconFontSize;
1355
1474
  }
1356
1475
  }
1357
- // Tính line height block height cho icon value
1358
- const valueLineHeight = effectiveIconValueFontSize;
1359
- let allWrappedLines = [];
1360
- // Text align center: căn giữa theo chiều dọc trong block
1361
- const textCenterY = cursorY + iconImageHeight / 2;
1362
- const valueStartX = x + labelWidth;
1363
- if (needsWrap) {
1364
- // Dùng wrap text logic
1365
- const wrappedLines = buildWrappedLines(ctx, iconValue, availableWidth, valueStartX, textCenterY, valueLineHeight, mockupBounds);
1366
- allWrappedLines = wrappedLines;
1367
- wrappedLines.length * valueLineHeight;
1368
- }
1369
- else {
1370
- // Không cần wrap, chỉ một dòng
1371
- allWrappedLines = [iconValue];
1372
- }
1373
- // Vẽ label với textBaseline = middle để align center với value
1476
+ // Bước 2: Nếu bước 1 thất bại, xuống dòng với font size gốc
1374
1477
  ctx.textBaseline = "middle";
1375
1478
  ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1376
1479
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
1377
1480
  ctx.fillText(iconLabel, x, textCenterY);
1378
- // Vẽ icon value với align center
1379
1481
  ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
1380
1482
  ctx.fillStyle = DEFAULT_ERROR_COLOR;
1381
1483
  let maxValueLineWidth = 0;
1382
- // Vẽ icon value, căn giữa theo chiều dọc
1484
+ let totalTextHeight = iconImageHeight;
1383
1485
  if (needsWrap) {
1384
- // nhiều dòng sau khi wrap
1385
- allWrappedLines.forEach((line, index) => {
1386
- const lineY = textCenterY - (allWrappedLines.length - 1) / 2 * valueLineHeight + index * valueLineHeight;
1387
- // Kiểm tra overlap với mockup và điều chỉnh text nếu cần
1388
- if (mockupBounds) {
1389
- const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds);
1390
- const lineWidth = ctx.measureText(line).width;
1391
- if (lineWidth > effectiveMaxWidth) {
1392
- // Cắt text cho đến khi vừa với effectiveMaxWidth
1393
- let truncatedLine = line;
1394
- while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
1395
- truncatedLine = truncatedLine.slice(0, -1);
1396
- }
1397
- ctx.fillText(truncatedLine, valueStartX, lineY);
1398
- const w = ctx.measureText(truncatedLine).width;
1399
- if (w > maxValueLineWidth)
1400
- maxValueLineWidth = w;
1401
- }
1402
- else {
1403
- ctx.fillText(line, valueStartX, lineY);
1404
- const w = ctx.measureText(line).width;
1405
- if (w > maxValueLineWidth)
1406
- maxValueLineWidth = w;
1486
+ // Bước 2: Xuống dòng với font size gốc
1487
+ const wrappedLines = buildWrappedLines(ctx, iconValue, effectiveMaxWidth, valueStartX, textTopY, lineHeight, mockupBounds);
1488
+ // Tính height dựa trên số dòng thực tế
1489
+ totalTextHeight = Math.max(iconImageHeight, wrappedLines.length * lineHeight);
1490
+ wrappedLines.forEach((line, index) => {
1491
+ const lineTopY = textTopY + index * lineHeight;
1492
+ const lineCenterY = lineTopY + lineHeight / 2;
1493
+ const lineEffectiveMaxWidth = mockupBounds
1494
+ ? getEffectiveMaxWidth(valueStartX, lineTopY, lineHeight, availableWidth, mockupBounds)
1495
+ : availableWidth;
1496
+ let lineToRender = line;
1497
+ const lineWidth = ctx.measureText(lineToRender).width;
1498
+ if (lineWidth > lineEffectiveMaxWidth) {
1499
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1500
+ lineToRender = lineToRender.slice(0, -1);
1407
1501
  }
1408
1502
  }
1409
- else {
1410
- ctx.fillText(line, valueStartX, lineY);
1411
- const w = ctx.measureText(line).width;
1412
- if (w > maxValueLineWidth)
1413
- maxValueLineWidth = w;
1414
- }
1503
+ ctx.fillText(lineToRender, valueStartX, lineCenterY);
1504
+ const w = ctx.measureText(lineToRender).width;
1505
+ if (w > maxValueLineWidth)
1506
+ maxValueLineWidth = w;
1415
1507
  });
1416
1508
  }
1417
1509
  else {
1418
- // Chỉ một dòng, căn giữa
1510
+ // Bước 1 thành công: Render với font size đã shrink (1 dòng)
1511
+ const shrunkTextTopY = textCenterY - effectiveIconValueFontSize / 2;
1512
+ const shrunkLineHeight = effectiveIconValueFontSize;
1513
+ const shrunkEffectiveMaxWidth = mockupBounds
1514
+ ? getEffectiveMaxWidth(valueStartX, shrunkTextTopY, shrunkLineHeight, availableWidth, mockupBounds)
1515
+ : availableWidth;
1419
1516
  const lineText = ` ${iconValue}`;
1420
- // Kiểm tra overlap với mockup và điều chỉnh text nếu cần
1421
- if (mockupBounds) {
1422
- const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, textCenterY, valueLineHeight, availableWidth, mockupBounds);
1423
- const lineWidth = ctx.measureText(lineText).width;
1424
- if (lineWidth > effectiveMaxWidth) {
1425
- // Cắt text cho đến khi vừa với effectiveMaxWidth
1426
- let truncatedLine = lineText;
1427
- while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
1428
- truncatedLine = truncatedLine.slice(0, -1);
1429
- }
1430
- ctx.fillText(truncatedLine, valueStartX, textCenterY);
1431
- const w = ctx.measureText(truncatedLine).width;
1432
- if (w > maxValueLineWidth)
1433
- maxValueLineWidth = w;
1434
- }
1435
- else {
1436
- ctx.fillText(lineText, valueStartX, textCenterY);
1437
- const w = ctx.measureText(lineText).width;
1438
- if (w > maxValueLineWidth)
1439
- maxValueLineWidth = w;
1517
+ let lineToRender = lineText;
1518
+ const lineWidth = ctx.measureText(lineToRender).width;
1519
+ if (lineWidth > shrunkEffectiveMaxWidth) {
1520
+ while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth && lineToRender.length > 0) {
1521
+ lineToRender = lineToRender.slice(0, -1);
1440
1522
  }
1441
1523
  }
1442
- else {
1443
- ctx.fillText(lineText, valueStartX, textCenterY);
1444
- const w = ctx.measureText(lineText).width;
1445
- if (w > maxValueLineWidth)
1446
- maxValueLineWidth = w;
1447
- }
1524
+ ctx.fillText(lineToRender, valueStartX, textCenterY);
1525
+ const w = ctx.measureText(lineToRender).width;
1526
+ if (w > maxValueLineWidth)
1527
+ maxValueLineWidth = w;
1448
1528
  }
1449
1529
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
1450
1530
  // Reset textBaseline về top cho các phần tiếp theo
1451
1531
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
1452
1532
  const iconResult = {
1453
- height: iconImageHeight + lineGap,
1533
+ height: totalTextHeight + lineGap,
1454
1534
  // tổng width của cả label + value, dùng để canh icon image lệch sang phải
1455
1535
  lastLineWidth: labelWidth + maxValueLineWidth};
1456
1536
  // Kiểm tra xem phần icon image có vượt quá canvas không trước khi render