embroidery-qc-image 1.0.25 → 1.0.26
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.esm.js +315 -100
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +315 -100
- package/dist/index.js.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: 160,
|
|
52
52
|
// Colors
|
|
53
53
|
HEADER_COLOR: "#000000",
|
|
54
54
|
LABEL_COLOR: "#444444",
|
|
@@ -62,15 +62,10 @@ const LAYOUT = {
|
|
|
62
62
|
SECTION_SPACING: 60,
|
|
63
63
|
ELEMENT_SPACING: 100,
|
|
64
64
|
SWATCH_SPACING: 25,
|
|
65
|
-
FLORAL_SPACING: 100,
|
|
66
65
|
// Visual styling
|
|
67
66
|
SWATCH_HEIGHT_RATIO: 2.025,
|
|
68
67
|
UNDERLINE_POSITION: 0.9,
|
|
69
|
-
UNDERLINE_WIDTH: 10
|
|
70
|
-
// Swatch reserved space
|
|
71
|
-
SWATCH_RESERVED_SPACE: 1000,
|
|
72
|
-
MIN_TEXT_WIDTH: 400,
|
|
73
|
-
};
|
|
68
|
+
UNDERLINE_WIDTH: 10};
|
|
74
69
|
// ============================================================================
|
|
75
70
|
// HELPER FUNCTIONS
|
|
76
71
|
// ============================================================================
|
|
@@ -388,6 +383,22 @@ const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
|
388
383
|
});
|
|
389
384
|
return result.length ? result : [""];
|
|
390
385
|
};
|
|
386
|
+
const calculateSwatchesWidth = (colors, swatchHeight, scaleFactor, imageRefs) => {
|
|
387
|
+
let totalWidth = 0;
|
|
388
|
+
colors.forEach((color, index) => {
|
|
389
|
+
const url = getImageUrl("threadColor", color);
|
|
390
|
+
const img = imageRefs.current.get(url);
|
|
391
|
+
if (img && img.complete && img.naturalHeight > 0) {
|
|
392
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
393
|
+
const swatchW = Math.max(1, Math.floor(swatchHeight * ratio));
|
|
394
|
+
totalWidth += swatchW;
|
|
395
|
+
if (index < colors.length - 1) {
|
|
396
|
+
totalWidth += LAYOUT.SWATCH_SPACING * scaleFactor;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
return totalWidth;
|
|
401
|
+
};
|
|
391
402
|
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
392
403
|
let swatchX = startX;
|
|
393
404
|
colors.forEach((color) => {
|
|
@@ -546,22 +557,6 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
546
557
|
imageRefs.current.set("mockup", mockupImage);
|
|
547
558
|
}
|
|
548
559
|
}
|
|
549
|
-
const floralAssets = [];
|
|
550
|
-
const seenFlorals = new Set();
|
|
551
|
-
config.sides.forEach((side) => {
|
|
552
|
-
side.positions.forEach((position) => {
|
|
553
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
554
|
-
const url = getImageUrl("floral", position.floral_pattern);
|
|
555
|
-
if (!seenFlorals.has(url)) {
|
|
556
|
-
const img = imageRefs.current.get(url);
|
|
557
|
-
if (img?.complete && img.naturalWidth > 0) {
|
|
558
|
-
floralAssets.push(img);
|
|
559
|
-
seenFlorals.add(url);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
560
|
const measureCanvas = document.createElement("canvas");
|
|
566
561
|
measureCanvas.width = canvas.width;
|
|
567
562
|
measureCanvas.height = canvas.height;
|
|
@@ -586,7 +581,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
586
581
|
measureY += sideHeight + measureSpacing;
|
|
587
582
|
});
|
|
588
583
|
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
|
|
589
|
-
drawMockupAndFlorals(ctx, canvas,
|
|
584
|
+
drawMockupAndFlorals(ctx, canvas, imageRefs);
|
|
590
585
|
// Render warning & message with scaleFactor and get actual heights
|
|
591
586
|
let actualWarningHeight = 0;
|
|
592
587
|
let actualMessageHeight = 0;
|
|
@@ -685,7 +680,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
|
|
|
685
680
|
// Return the actual height of the warning (number of lines * lineHeight)
|
|
686
681
|
return lines.length * lineHeight;
|
|
687
682
|
};
|
|
688
|
-
const drawMockupAndFlorals = (ctx, canvas,
|
|
683
|
+
const drawMockupAndFlorals = (ctx, canvas, imageRefs) => {
|
|
689
684
|
const mockupImg = imageRefs.current.get("mockup");
|
|
690
685
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
691
686
|
return;
|
|
@@ -698,19 +693,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
|
698
693
|
const x = canvas.width - margin - width;
|
|
699
694
|
const y = canvas.height - margin - height;
|
|
700
695
|
ctx.drawImage(mockupImg, x, y, width, height);
|
|
701
|
-
//
|
|
702
|
-
if (floralAssets.length > 0) {
|
|
703
|
-
const floralH = Math.min(500, height);
|
|
704
|
-
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
705
|
-
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
706
|
-
const img = floralAssets[i];
|
|
707
|
-
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
|
|
708
|
-
const w = Math.max(1, Math.floor(floralH * ratio));
|
|
709
|
-
currentX -= w;
|
|
710
|
-
ctx.drawImage(img, currentX, y + height - floralH, w, floralH);
|
|
711
|
-
currentX -= LAYOUT.FLORAL_SPACING;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
696
|
+
// Bỏ phần vẽ florals cạnh mockup vì đã hiển thị cạnh text rồi
|
|
714
697
|
};
|
|
715
698
|
const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
716
699
|
let currentY = startY;
|
|
@@ -732,8 +715,33 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
732
715
|
ctx.stroke();
|
|
733
716
|
currentY += headerResult.height + LAYOUT.SECTION_SPACING * scaleFactor;
|
|
734
717
|
ctx.restore();
|
|
735
|
-
//
|
|
718
|
+
// Kiểm tra xem có phải trường hợp "không thêu gì" không
|
|
736
719
|
const textPositions = side.positions.filter((p) => p.type === "TEXT");
|
|
720
|
+
const iconPositions = side.positions.filter((p) => p.type === "ICON");
|
|
721
|
+
// Kiểm tra tất cả TEXT positions có trống không
|
|
722
|
+
// Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
|
|
723
|
+
const allTextEmpty = textPositions.length === 0 || textPositions.every((p) => {
|
|
724
|
+
const text = p.text ?? "";
|
|
725
|
+
return text.trim() === "";
|
|
726
|
+
});
|
|
727
|
+
// Kiểm tra tất cả ICON positions có is_delete_icon = true không
|
|
728
|
+
// Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
|
|
729
|
+
const allIconsDeleted = iconPositions.length === 0 || iconPositions.every((p) => {
|
|
730
|
+
return p.is_delete_icon === true;
|
|
731
|
+
});
|
|
732
|
+
// Nếu tất cả TEXT trống và tất cả ICON bị xóa, chỉ render dòng "(không thêu gì)"
|
|
733
|
+
if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
|
|
734
|
+
ctx.save();
|
|
735
|
+
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
736
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
737
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
738
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
739
|
+
ctx.fillText("(không thêu gì)", padding, currentY);
|
|
740
|
+
currentY += otherFontSize + lineGap;
|
|
741
|
+
ctx.restore();
|
|
742
|
+
return currentY - startY;
|
|
743
|
+
}
|
|
744
|
+
// Compute uniform properties
|
|
737
745
|
const iconColorPositions = side.positions.filter((p) => p.type === "ICON" && (!p.layer_colors?.length || p.layer_colors.length === 1));
|
|
738
746
|
const iconColorValues = iconColorPositions
|
|
739
747
|
.map((p) => {
|
|
@@ -918,23 +926,87 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
918
926
|
rendered++;
|
|
919
927
|
}
|
|
920
928
|
if (values.color && values.color !== "None" && shouldRenderField("color")) {
|
|
921
|
-
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
922
|
-
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
923
|
-
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
924
|
-
const swatchX = x +
|
|
925
|
-
Math.ceil(result.lastLineWidth) +
|
|
926
|
-
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
927
|
-
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
928
929
|
const colors = values.color.includes(",")
|
|
929
930
|
? values.color.split(",").map((s) => s.trim())
|
|
930
931
|
: [values.color];
|
|
932
|
+
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
933
|
+
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
934
|
+
// Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
|
|
935
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
936
|
+
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
937
|
+
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
938
|
+
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
939
|
+
const swatchesStartX = textEndX + spacing;
|
|
940
|
+
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
941
|
+
const shouldWrapSwatches = swatchesEndX > x + maxWidth;
|
|
942
|
+
let swatchX;
|
|
943
|
+
let swatchY;
|
|
944
|
+
if (shouldWrapSwatches) {
|
|
945
|
+
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
946
|
+
swatchX = x;
|
|
947
|
+
swatchY = result.lastLineY + fontSize + lineGap;
|
|
948
|
+
cursorY += result.height + fontSize + lineGap;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
952
|
+
swatchX = swatchesStartX;
|
|
953
|
+
swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
954
|
+
cursorY += result.height;
|
|
955
|
+
}
|
|
931
956
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
932
|
-
|
|
957
|
+
if (shouldWrapSwatches) {
|
|
958
|
+
cursorY += swatchH;
|
|
959
|
+
}
|
|
933
960
|
rendered++;
|
|
934
961
|
}
|
|
935
962
|
if (values.floral && values.floral !== "None" && shouldRenderField("floral")) {
|
|
936
|
-
const
|
|
937
|
-
|
|
963
|
+
const floralUrl = getImageUrl("floral", values.floral);
|
|
964
|
+
const floralImg = imageRefs.current.get(floralUrl);
|
|
965
|
+
// Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
|
|
966
|
+
const floralH = fontSize * 2.5;
|
|
967
|
+
let totalFloralWidth = 0;
|
|
968
|
+
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
969
|
+
const ratio = floralImg.naturalWidth / floralImg.naturalHeight;
|
|
970
|
+
totalFloralWidth = Math.max(1, Math.floor(floralH * ratio));
|
|
971
|
+
}
|
|
972
|
+
// Line height giống icon_image: floralH + lineGap
|
|
973
|
+
const floralLineHeight = floralH + lineGap;
|
|
974
|
+
// Text align bottom: đặt text ở dưới cùng của dòng
|
|
975
|
+
const textBottomY = cursorY + floralH;
|
|
976
|
+
// Đo width trước khi vẽ
|
|
977
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
978
|
+
const labelText = `Mẫu hoa: ${values.floral}`;
|
|
979
|
+
const labelWidth = ctx.measureText(labelText).width;
|
|
980
|
+
// Vẽ text với textBaseline = bottom
|
|
981
|
+
ctx.textBaseline = "bottom";
|
|
982
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
983
|
+
ctx.fillText(labelText, x, textBottomY);
|
|
984
|
+
// Reset textBaseline về top cho các phần tiếp theo
|
|
985
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
986
|
+
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
987
|
+
const textEndX = x + labelWidth;
|
|
988
|
+
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
989
|
+
const floralStartX = textEndX + spacing;
|
|
990
|
+
const floralEndX = floralStartX + totalFloralWidth;
|
|
991
|
+
const shouldWrapFloral = floralEndX > x + maxWidth;
|
|
992
|
+
let floralX;
|
|
993
|
+
let floralY;
|
|
994
|
+
if (shouldWrapFloral) {
|
|
995
|
+
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
996
|
+
floralX = x;
|
|
997
|
+
floralY = textBottomY + lineGap;
|
|
998
|
+
cursorY += floralLineHeight;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1002
|
+
floralX = floralStartX;
|
|
1003
|
+
floralY = textBottomY - floralH; // Align bottom với text
|
|
1004
|
+
cursorY += floralLineHeight;
|
|
1005
|
+
}
|
|
1006
|
+
// Vẽ ảnh floral
|
|
1007
|
+
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
1008
|
+
ctx.drawImage(floralImg, floralX, floralY, totalFloralWidth, floralH);
|
|
1009
|
+
}
|
|
938
1010
|
rendered++;
|
|
939
1011
|
}
|
|
940
1012
|
if (rendered > 0)
|
|
@@ -942,40 +1014,82 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
942
1014
|
ctx.restore();
|
|
943
1015
|
return cursorY - y;
|
|
944
1016
|
};
|
|
945
|
-
const renderTextPosition = (ctx, position, x, y, maxWidth,
|
|
1017
|
+
const renderTextPosition = (ctx, position, x, y, maxWidth, // tổng chiều rộng usable (không tính padding ngoài)
|
|
1018
|
+
displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
946
1019
|
ctx.save();
|
|
947
1020
|
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
948
1021
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
949
1022
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
950
1023
|
let currentY = y;
|
|
951
1024
|
let drawnHeight = 0;
|
|
952
|
-
//
|
|
1025
|
+
// Chuẩn hóa xuống dòng:
|
|
1026
|
+
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1027
|
+
const normalizeNewlines = (text) => text
|
|
1028
|
+
.replace(/\r\n/g, "\n")
|
|
1029
|
+
.replace(/\r/g, "\n")
|
|
1030
|
+
.replace(/\\n/g, "\n");
|
|
1031
|
+
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1032
|
+
const rawOriginalText = position.text ?? "";
|
|
1033
|
+
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
1034
|
+
const isEmptyText = normalizedText.trim() === "";
|
|
1035
|
+
// ===========================================================================
|
|
1036
|
+
// PHẦN TEXT CHÍNH
|
|
1037
|
+
// - Giữ nguyên format: chỉ xuống dòng khi có '\n'
|
|
1038
|
+
// - Không tự wrap theo maxWidth
|
|
1039
|
+
// - Nếu tổng chiều ngang > maxWidth, tự động giảm font-size để vừa
|
|
1040
|
+
// ===========================================================================
|
|
1041
|
+
// Label "Text N: " luôn là font mặc định (không bị co theo nội dung)
|
|
953
1042
|
const textLabel = `Text ${displayIndex}: `;
|
|
954
1043
|
ctx.font = `bold ${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
955
1044
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
956
1045
|
const labelWidth = ctx.measureText(textLabel).width;
|
|
957
1046
|
ctx.fillText(textLabel, x, currentY);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const
|
|
961
|
-
//
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1047
|
+
// Phần text value bắt đầu sau label
|
|
1048
|
+
const valueStartX = x + labelWidth;
|
|
1049
|
+
const availableWidth = Math.max(1, maxWidth - labelWidth);
|
|
1050
|
+
// Chuẩn hóa nội dung text để render
|
|
1051
|
+
const rawText = isEmptyText ? "(không có text)" : normalizedText;
|
|
1052
|
+
const lines = rawText.split("\n");
|
|
1053
|
+
// Tính font-size hiệu dụng cho phần value sao cho:
|
|
1054
|
+
// - Không vượt quá availableWidth
|
|
1055
|
+
// - Có thể thu nhỏ tùy ý (theo yêu cầu, không giới hạn tối thiểu)
|
|
1056
|
+
const measureMaxLineWidth = (fontSize) => {
|
|
1057
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1058
|
+
let maxLineWidth = 0;
|
|
1059
|
+
lines.forEach((line) => {
|
|
1060
|
+
const w = ctx.measureText(line).width;
|
|
1061
|
+
if (w > maxLineWidth)
|
|
1062
|
+
maxLineWidth = w;
|
|
1063
|
+
});
|
|
1064
|
+
return maxLineWidth;
|
|
1065
|
+
};
|
|
1066
|
+
let effectiveTextFontSize = textFontSize;
|
|
1067
|
+
if (!isEmptyText) {
|
|
1068
|
+
const baseMaxWidth = measureMaxLineWidth(textFontSize);
|
|
1069
|
+
if (baseMaxWidth > availableWidth) {
|
|
1070
|
+
const shrinkRatio = availableWidth / baseMaxWidth;
|
|
1071
|
+
effectiveTextFontSize = textFontSize * shrinkRatio;
|
|
1072
|
+
}
|
|
973
1073
|
}
|
|
1074
|
+
// Vẽ phần value với font hiệu dụng, màu đỏ
|
|
1075
|
+
ctx.font = `${effectiveTextFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1076
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1077
|
+
const valueLineHeight = effectiveTextFontSize; // giữ giống wrapText cũ (lineHeight = fontSize)
|
|
1078
|
+
let localY = currentY;
|
|
1079
|
+
lines.forEach((line, idx) => {
|
|
1080
|
+
ctx.fillText(line, valueStartX, localY);
|
|
1081
|
+
localY += valueLineHeight;
|
|
1082
|
+
});
|
|
1083
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1084
|
+
currentY += textBlockHeight;
|
|
1085
|
+
drawnHeight += textBlockHeight;
|
|
974
1086
|
// Draw additional labels (skip when text is empty)
|
|
975
1087
|
if (!isEmptyText) {
|
|
976
1088
|
currentY += lineGap;
|
|
977
1089
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
978
1090
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1091
|
+
// Lưu ý: phần dưới này vẫn có thể wrap theo maxWidth vì đây chỉ là label mô tả,
|
|
1092
|
+
// không phải nội dung Text chính cần giữ nguyên format.
|
|
979
1093
|
if (showLabels.shape && position.text_shape) {
|
|
980
1094
|
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
981
1095
|
currentY += result.height;
|
|
@@ -1006,23 +1120,90 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
1006
1120
|
if (showLabels.color) {
|
|
1007
1121
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
1008
1122
|
if (colorValue) {
|
|
1009
|
-
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
1010
|
-
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
1011
|
-
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1012
|
-
const swatchX = x +
|
|
1013
|
-
Math.ceil(result.lastLineWidth) +
|
|
1014
|
-
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1015
|
-
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1016
1123
|
const colors = position.character_colors || [position.color];
|
|
1124
|
+
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1125
|
+
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
1126
|
+
// Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
|
|
1127
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
1128
|
+
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1129
|
+
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
1130
|
+
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1131
|
+
const swatchesStartX = textEndX + spacing;
|
|
1132
|
+
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1133
|
+
const shouldWrapSwatches = swatchesEndX > x + maxWidth;
|
|
1134
|
+
let swatchX;
|
|
1135
|
+
let swatchY;
|
|
1136
|
+
if (shouldWrapSwatches) {
|
|
1137
|
+
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
1138
|
+
swatchX = x;
|
|
1139
|
+
swatchY = result.lastLineY + otherFontSize + lineGap;
|
|
1140
|
+
currentY += result.height + otherFontSize + lineGap;
|
|
1141
|
+
drawnHeight += result.height + otherFontSize + lineGap;
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1145
|
+
swatchX = swatchesStartX;
|
|
1146
|
+
swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1147
|
+
currentY += result.height;
|
|
1148
|
+
drawnHeight += result.height;
|
|
1149
|
+
}
|
|
1017
1150
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1018
|
-
|
|
1019
|
-
|
|
1151
|
+
if (shouldWrapSwatches) {
|
|
1152
|
+
currentY += swatchH;
|
|
1153
|
+
drawnHeight += swatchH;
|
|
1154
|
+
}
|
|
1020
1155
|
}
|
|
1021
1156
|
}
|
|
1022
1157
|
if (showLabels.floral && position.floral_pattern) {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1158
|
+
const floralUrl = getImageUrl("floral", position.floral_pattern);
|
|
1159
|
+
const floralImg = imageRefs.current.get(floralUrl);
|
|
1160
|
+
// Tính kích thước ảnh floral (thêm 50% = 2.5x otherFontSize)
|
|
1161
|
+
const floralH = otherFontSize * 2.5;
|
|
1162
|
+
let totalFloralWidth = 0;
|
|
1163
|
+
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
1164
|
+
const ratio = floralImg.naturalWidth / floralImg.naturalHeight;
|
|
1165
|
+
totalFloralWidth = Math.max(1, Math.floor(floralH * ratio));
|
|
1166
|
+
}
|
|
1167
|
+
// Line height giống icon_image: floralH + lineGap
|
|
1168
|
+
const floralLineHeight = floralH + lineGap;
|
|
1169
|
+
// Text align bottom: đặt text ở dưới cùng của dòng
|
|
1170
|
+
const textBottomY = currentY + floralH;
|
|
1171
|
+
// Đo width trước khi vẽ
|
|
1172
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1173
|
+
const labelText = `Mẫu hoa: ${position.floral_pattern}`;
|
|
1174
|
+
const labelWidth = ctx.measureText(labelText).width;
|
|
1175
|
+
// Vẽ text với textBaseline = bottom
|
|
1176
|
+
ctx.textBaseline = "bottom";
|
|
1177
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1178
|
+
ctx.fillText(labelText, x, textBottomY);
|
|
1179
|
+
// Reset textBaseline về top cho các phần tiếp theo
|
|
1180
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1181
|
+
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
1182
|
+
const textEndX = x + labelWidth;
|
|
1183
|
+
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1184
|
+
const floralStartX = textEndX + spacing;
|
|
1185
|
+
const floralEndX = floralStartX + totalFloralWidth;
|
|
1186
|
+
const shouldWrapFloral = floralEndX > x + maxWidth;
|
|
1187
|
+
let floralX;
|
|
1188
|
+
let floralY;
|
|
1189
|
+
if (shouldWrapFloral) {
|
|
1190
|
+
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
1191
|
+
floralX = x;
|
|
1192
|
+
floralY = textBottomY + lineGap;
|
|
1193
|
+
currentY += floralLineHeight;
|
|
1194
|
+
drawnHeight += floralLineHeight;
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1198
|
+
floralX = floralStartX;
|
|
1199
|
+
floralY = textBottomY - floralH; // Align bottom với text
|
|
1200
|
+
currentY += floralLineHeight;
|
|
1201
|
+
drawnHeight += floralLineHeight;
|
|
1202
|
+
}
|
|
1203
|
+
// Vẽ ảnh floral
|
|
1204
|
+
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
1205
|
+
ctx.drawImage(floralImg, floralX, floralY, totalFloralWidth, floralH);
|
|
1206
|
+
}
|
|
1026
1207
|
}
|
|
1027
1208
|
}
|
|
1028
1209
|
ctx.restore();
|
|
@@ -1057,36 +1238,46 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1057
1238
|
// Fallback: hiển thị mã icon (ép sang string)
|
|
1058
1239
|
iconValue = String(position.icon);
|
|
1059
1240
|
}
|
|
1060
|
-
//
|
|
1241
|
+
// Kiểm tra xem có icon_image không để tính height phù hợp
|
|
1242
|
+
const hasIconImage = position.icon_image && position.icon_image.trim().length > 0;
|
|
1243
|
+
const iconImageHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
|
|
1244
|
+
// Text align bottom: đặt text ở dưới cùng của dòng
|
|
1245
|
+
const textBottomY = cursorY + iconImageHeight;
|
|
1246
|
+
// Đo width trước khi vẽ
|
|
1061
1247
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1062
|
-
ctx.fillText(iconLabel, x, cursorY);
|
|
1063
1248
|
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1064
|
-
// Vẽ value kế bên, font thường, màu đỏ giống text value
|
|
1065
1249
|
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1066
|
-
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1067
1250
|
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
1251
|
const valueWidth = ctx.measureText(valueText).width;
|
|
1252
|
+
// Vẽ text với textBaseline = bottom
|
|
1253
|
+
ctx.textBaseline = "bottom";
|
|
1254
|
+
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1255
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1256
|
+
ctx.fillText(iconLabel, x, textBottomY);
|
|
1257
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1258
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1259
|
+
ctx.fillText(valueText, x + labelWidth, textBottomY);
|
|
1260
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1261
|
+
// Reset textBaseline về top cho các phần tiếp theo
|
|
1262
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1072
1263
|
const iconResult = {
|
|
1073
|
-
height:
|
|
1264
|
+
height: iconImageHeight + lineGap,
|
|
1074
1265
|
// 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
|
-
};
|
|
1266
|
+
lastLineWidth: labelWidth + valueWidth};
|
|
1078
1267
|
// Draw icon image
|
|
1079
1268
|
const iconUrl = getIconImageUrl(position);
|
|
1080
1269
|
if (iconUrl) {
|
|
1081
1270
|
const img = imageRefs.current.get(iconUrl);
|
|
1082
1271
|
if (img?.complete && img.naturalHeight > 0) {
|
|
1083
1272
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1084
|
-
|
|
1273
|
+
// Nếu có icon_image thì hiển thị to gấp đôi
|
|
1274
|
+
const iconHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
|
|
1275
|
+
const swatchW = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1085
1276
|
const iconX = x +
|
|
1086
1277
|
Math.ceil(iconResult.lastLineWidth) +
|
|
1087
1278
|
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1088
|
-
const iconY =
|
|
1089
|
-
ctx.drawImage(img, iconX, iconY, swatchW,
|
|
1279
|
+
const iconY = textBottomY - iconHeight; // Align bottom với text
|
|
1280
|
+
ctx.drawImage(img, iconX, iconY, swatchW, iconHeight);
|
|
1090
1281
|
}
|
|
1091
1282
|
}
|
|
1092
1283
|
cursorY += iconResult.height;
|
|
@@ -1103,15 +1294,39 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1103
1294
|
const hasMultiLayerColors = layerCount > 1;
|
|
1104
1295
|
const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;
|
|
1105
1296
|
if (iconColors?.length && !shouldSkipColorSection) {
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
const swatchH = Math.floor(
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1297
|
+
// Dòng "Màu chỉ:" của icon dùng OTHER_FONT_SIZE, không dùng iconFontSize
|
|
1298
|
+
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1299
|
+
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1300
|
+
const totalSwatchWidth = calculateSwatchesWidth(iconColors, swatchH, scaleFactor, imageRefs);
|
|
1301
|
+
// Set font về OTHER_FONT_SIZE trước khi vẽ text "Màu chỉ:"
|
|
1302
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1303
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1304
|
+
// Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
|
|
1305
|
+
const colorResult = wrapText(ctx, `Màu chỉ: ${iconColors.join(", ")}`, x, cursorY, maxWidth, otherFontSize + lineGap);
|
|
1306
|
+
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1307
|
+
const textEndX = x + Math.ceil(colorResult.lastLineWidth);
|
|
1308
|
+
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1309
|
+
const swatchesStartX = textEndX + spacing;
|
|
1310
|
+
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1311
|
+
const shouldWrapSwatches = swatchesEndX > x + maxWidth;
|
|
1312
|
+
let swatchX;
|
|
1313
|
+
let swatchY;
|
|
1314
|
+
if (shouldWrapSwatches) {
|
|
1315
|
+
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
1316
|
+
swatchX = x;
|
|
1317
|
+
swatchY = colorResult.lastLineY + otherFontSize + lineGap;
|
|
1318
|
+
cursorY += colorResult.height + otherFontSize + lineGap;
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1322
|
+
swatchX = swatchesStartX;
|
|
1323
|
+
swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1324
|
+
cursorY += colorResult.height;
|
|
1325
|
+
}
|
|
1113
1326
|
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1114
|
-
|
|
1327
|
+
if (shouldWrapSwatches) {
|
|
1328
|
+
cursorY += swatchH;
|
|
1329
|
+
}
|
|
1115
1330
|
}
|
|
1116
1331
|
ctx.restore();
|
|
1117
1332
|
return cursorY - y;
|