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