embroidery-qc-image 1.0.26 → 1.0.28
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 +365 -156
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +365 -156
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -329,24 +329,44 @@ const preloadImages = async (config, imageRefs) => {
|
|
|
329
329
|
return;
|
|
330
330
|
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
331
331
|
};
|
|
332
|
-
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
332
|
+
const wrapText = (ctx, text, x, y, maxWidth, lineHeight, mockupBounds = null) => {
|
|
333
333
|
const words = text.split(" ");
|
|
334
334
|
const lines = [];
|
|
335
335
|
let currentLine = words[0];
|
|
336
|
+
let currentY = y;
|
|
336
337
|
for (let i = 1; i < words.length; i++) {
|
|
337
338
|
const testLine = currentLine + " " + words[i];
|
|
338
|
-
|
|
339
|
+
// Tính maxWidth hiệu dụng cho dòng hiện tại
|
|
340
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(x, currentY, lineHeight, maxWidth, mockupBounds);
|
|
341
|
+
const testWidth = ctx.measureText(testLine).width;
|
|
342
|
+
// Kiểm tra overlap với mockup
|
|
343
|
+
let shouldWrap = false;
|
|
344
|
+
if (testWidth > effectiveMaxWidth && currentLine.length > 0) {
|
|
345
|
+
shouldWrap = true;
|
|
346
|
+
}
|
|
347
|
+
if (shouldWrap) {
|
|
339
348
|
lines.push(currentLine);
|
|
340
349
|
currentLine = words[i];
|
|
350
|
+
currentY += lineHeight;
|
|
341
351
|
}
|
|
342
352
|
else {
|
|
343
353
|
currentLine = testLine;
|
|
344
354
|
}
|
|
345
355
|
}
|
|
346
356
|
lines.push(currentLine);
|
|
347
|
-
|
|
357
|
+
currentY = y;
|
|
348
358
|
lines.forEach((line) => {
|
|
349
|
-
|
|
359
|
+
// Tính maxWidth hiệu dụng cho từng dòng khi render
|
|
360
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(x, currentY, lineHeight, maxWidth, mockupBounds);
|
|
361
|
+
// Nếu dòng quá dài, cắt bớt
|
|
362
|
+
let lineToRender = line;
|
|
363
|
+
if (ctx.measureText(line).width > effectiveMaxWidth) {
|
|
364
|
+
// Cắt từng ký tự cho đến khi vừa
|
|
365
|
+
while (ctx.measureText(lineToRender).width > effectiveMaxWidth && lineToRender.length > 0) {
|
|
366
|
+
lineToRender = lineToRender.slice(0, -1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
ctx.fillText(lineToRender, x, currentY);
|
|
350
370
|
currentY += lineHeight;
|
|
351
371
|
});
|
|
352
372
|
return {
|
|
@@ -355,29 +375,46 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
355
375
|
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
356
376
|
};
|
|
357
377
|
};
|
|
358
|
-
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
378
|
+
const buildWrappedLines = (ctx, text, maxWidth, startX = 0, startY = 0, lineHeight = 0, mockupBounds = null) => {
|
|
359
379
|
// Mỗi '\n' tương đương với một line break giống như khi wrap tự động.
|
|
360
380
|
const segments = text.split("\n");
|
|
361
381
|
const result = [];
|
|
382
|
+
let currentY = startY;
|
|
362
383
|
segments.forEach((segment) => {
|
|
363
384
|
const words = segment.split(" ").filter((word) => word.length > 0);
|
|
364
385
|
if (words.length === 0) {
|
|
365
386
|
// Nếu đoạn rỗng, thêm một dòng trống (break đúng 1 line)
|
|
366
387
|
result.push("");
|
|
388
|
+
if (lineHeight > 0)
|
|
389
|
+
currentY += lineHeight;
|
|
367
390
|
return;
|
|
368
391
|
}
|
|
369
392
|
let currentLine = words[0];
|
|
370
393
|
for (let i = 1; i < words.length; i++) {
|
|
371
394
|
const testLine = `${currentLine} ${words[i]}`;
|
|
372
|
-
|
|
395
|
+
// Tính maxWidth hiệu dụng cho dòng hiện tại
|
|
396
|
+
const effectiveMaxWidth = lineHeight > 0
|
|
397
|
+
? getEffectiveMaxWidth(startX, currentY, lineHeight, maxWidth, mockupBounds)
|
|
398
|
+
: maxWidth;
|
|
399
|
+
const testWidth = ctx.measureText(testLine).width;
|
|
400
|
+
// Kiểm tra overlap với mockup
|
|
401
|
+
let shouldWrap = false;
|
|
402
|
+
if (testWidth > effectiveMaxWidth && currentLine.length > 0) {
|
|
403
|
+
shouldWrap = true;
|
|
404
|
+
}
|
|
405
|
+
if (shouldWrap) {
|
|
373
406
|
result.push(currentLine);
|
|
374
407
|
currentLine = words[i];
|
|
408
|
+
if (lineHeight > 0)
|
|
409
|
+
currentY += lineHeight;
|
|
375
410
|
}
|
|
376
411
|
else {
|
|
377
412
|
currentLine = testLine;
|
|
378
413
|
}
|
|
379
414
|
}
|
|
380
415
|
result.push(currentLine);
|
|
416
|
+
if (lineHeight > 0)
|
|
417
|
+
currentY += lineHeight;
|
|
381
418
|
});
|
|
382
419
|
return result.length ? result : [""];
|
|
383
420
|
};
|
|
@@ -512,49 +549,14 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
512
549
|
return;
|
|
513
550
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
514
551
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
515
|
-
// Calculate warning & message height (with scaleFactor = 1 for measurement)
|
|
516
|
-
let warningHeight = 0;
|
|
517
|
-
let warningLineHeight = 0;
|
|
518
|
-
let warningLineCount = 0;
|
|
519
|
-
if (config.warning_message) {
|
|
520
|
-
const measureWarningCanvas = document.createElement("canvas");
|
|
521
|
-
measureWarningCanvas.width = canvas.width;
|
|
522
|
-
measureWarningCanvas.height = canvas.height;
|
|
523
|
-
const measureWarningCtx = measureWarningCanvas.getContext("2d");
|
|
524
|
-
if (measureWarningCtx) {
|
|
525
|
-
measureWarningCtx.textAlign = "left";
|
|
526
|
-
measureWarningCtx.textBaseline = "top";
|
|
527
|
-
measureWarningCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
|
|
528
|
-
const warningLines = buildWrappedLines(measureWarningCtx, `Note: ${config.warning_message.trim()}`, canvas.width - LAYOUT.PADDING * 4);
|
|
529
|
-
warningLineCount = warningLines.length;
|
|
530
|
-
warningLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
|
|
531
|
-
warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
let messageHeight = 0;
|
|
535
|
-
let messageLineHeight = 0;
|
|
536
|
-
let messageLineCount = 0;
|
|
537
|
-
if (config.message) {
|
|
538
|
-
const measureMessageCanvas = document.createElement("canvas");
|
|
539
|
-
measureMessageCanvas.width = canvas.width;
|
|
540
|
-
measureMessageCanvas.height = canvas.height;
|
|
541
|
-
const measureMessageCtx = measureMessageCanvas.getContext("2d");
|
|
542
|
-
if (measureMessageCtx) {
|
|
543
|
-
measureMessageCtx.textAlign = "left";
|
|
544
|
-
measureMessageCtx.textBaseline = "top";
|
|
545
|
-
measureMessageCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
|
|
546
|
-
const messageLines = buildWrappedLines(measureMessageCtx, config.message.trim(), canvas.width - LAYOUT.PADDING * 4);
|
|
547
|
-
messageLineCount = messageLines.length;
|
|
548
|
-
messageLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
|
|
549
|
-
messageHeight = messageLineCount * messageLineHeight + LAYOUT.PADDING;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
552
|
if (config.image_url) {
|
|
553
553
|
const mockupImage = imageRefs.current.get(config.image_url);
|
|
554
554
|
if (mockupImage) {
|
|
555
555
|
imageRefs.current.set("mockup", mockupImage);
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
+
// Tính mockup bounds để tránh text overlap
|
|
559
|
+
const mockupBounds = getMockupBounds(canvas, imageRefs);
|
|
558
560
|
const measureCanvas = document.createElement("canvas");
|
|
559
561
|
measureCanvas.width = canvas.width;
|
|
560
562
|
measureCanvas.height = canvas.height;
|
|
@@ -563,23 +565,48 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
563
565
|
return;
|
|
564
566
|
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
565
567
|
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
566
|
-
let measureY = LAYOUT.PADDING;
|
|
567
568
|
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
569
|
+
// Tìm scaleFactor tối ưu bằng binary search trong [0, 1]
|
|
570
|
+
const maxIterations = 12;
|
|
571
|
+
const epsilon = 0.001; // độ chính xác cho khoảng cách low-high
|
|
572
|
+
const contentHeight = canvas.height - LAYOUT.PADDING;
|
|
573
|
+
let low = 0;
|
|
574
|
+
let high = 1;
|
|
575
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
576
|
+
const testScale = (low + high) / 2;
|
|
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
|
+
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;
|
|
581
|
+
if (config.warning_message) {
|
|
582
|
+
const testWarningHeight = renderWarning(measureCtx, measureCanvas, config.warning_message, testScale);
|
|
583
|
+
testMeasureY += testWarningHeight;
|
|
584
|
+
}
|
|
585
|
+
if (config.message) {
|
|
586
|
+
const testMessageHeight = renderWarning(measureCtx, measureCanvas, config.message, testScale, 0, "", DEFAULT_ERROR_COLOR);
|
|
587
|
+
testMeasureY += testMessageHeight;
|
|
588
|
+
}
|
|
589
|
+
// Đo lại chiều cao của các sides với scaleFactor, tiếp nối sau warning/message
|
|
590
|
+
config.sides.forEach((side) => {
|
|
591
|
+
const sideHeight = renderSide(measureCtx, side, testMeasureY, canvas.width, canvas.height, testScale, imageRefs, mockupBounds);
|
|
592
|
+
testMeasureY += sideHeight + measureSpacing * testScale;
|
|
593
|
+
});
|
|
594
|
+
// Tổng chiều cao content (không gồm padding)
|
|
595
|
+
const totalHeight = testMeasureY;
|
|
596
|
+
if (totalHeight > contentHeight) {
|
|
597
|
+
// Content đang cao hơn vùng cho phép -> giảm scale
|
|
598
|
+
high = testScale;
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// Content vẫn fit trong vùng cho phép -> có thể tăng scale
|
|
602
|
+
low = testScale;
|
|
603
|
+
}
|
|
604
|
+
if (high - low < epsilon) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
576
607
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
measureY += sideHeight + measureSpacing;
|
|
580
|
-
});
|
|
581
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
|
|
582
|
-
drawMockupAndFlorals(ctx, canvas, imageRefs);
|
|
608
|
+
const scaleFactor = low;
|
|
609
|
+
drawMockup(ctx, canvas, imageRefs);
|
|
583
610
|
// Render warning & message with scaleFactor and get actual heights
|
|
584
611
|
let actualWarningHeight = 0;
|
|
585
612
|
let actualMessageHeight = 0;
|
|
@@ -592,7 +619,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
592
619
|
);
|
|
593
620
|
}
|
|
594
621
|
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
595
|
-
let currentY = LAYOUT.PADDING
|
|
622
|
+
let currentY = LAYOUT.PADDING;
|
|
596
623
|
if (config.warning_message && actualWarningHeight > 0) {
|
|
597
624
|
currentY += actualWarningHeight;
|
|
598
625
|
}
|
|
@@ -600,7 +627,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
600
627
|
currentY += actualMessageHeight;
|
|
601
628
|
}
|
|
602
629
|
config.sides.forEach((side) => {
|
|
603
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
630
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, canvas.height, scaleFactor, imageRefs, mockupBounds);
|
|
604
631
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
605
632
|
});
|
|
606
633
|
};
|
|
@@ -678,10 +705,10 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
|
|
|
678
705
|
// Return the actual height of the warning (number of lines * lineHeight)
|
|
679
706
|
return lines.length * lineHeight;
|
|
680
707
|
};
|
|
681
|
-
const
|
|
708
|
+
const getMockupBounds = (canvas, imageRefs) => {
|
|
682
709
|
const mockupImg = imageRefs.current.get("mockup");
|
|
683
710
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
684
|
-
return;
|
|
711
|
+
return null;
|
|
685
712
|
const margin = LAYOUT.PADDING;
|
|
686
713
|
const maxWidth = Math.min(1800, canvas.width * 0.375);
|
|
687
714
|
const maxHeight = canvas.height * 0.375;
|
|
@@ -690,10 +717,38 @@ const drawMockupAndFlorals = (ctx, canvas, imageRefs) => {
|
|
|
690
717
|
const height = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
691
718
|
const x = canvas.width - margin - width;
|
|
692
719
|
const y = canvas.height - margin - height;
|
|
693
|
-
|
|
694
|
-
|
|
720
|
+
return { x, y, width, height };
|
|
721
|
+
};
|
|
722
|
+
// Helper function để tính maxWidth hiệu dụng dựa trên vị trí Y và mockupBounds
|
|
723
|
+
const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds) => {
|
|
724
|
+
if (!mockupBounds)
|
|
725
|
+
return originalMaxWidth;
|
|
726
|
+
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
727
|
+
const lineTopY = y;
|
|
728
|
+
const lineBottomY = y + lineHeight;
|
|
729
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
|
|
730
|
+
if (overlapsY) {
|
|
731
|
+
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
732
|
+
const maxAllowedWidth = mockupBounds.x - x;
|
|
733
|
+
// Chỉ giới hạn nếu maxAllowedWidth > 0 và nhỏ hơn originalMaxWidth
|
|
734
|
+
if (maxAllowedWidth > 0 && maxAllowedWidth < originalMaxWidth) {
|
|
735
|
+
return maxAllowedWidth;
|
|
736
|
+
}
|
|
737
|
+
// Nếu x >= mockupBounds.x, text đã nằm sau mockup, không cần giới hạn
|
|
738
|
+
// Hoặc nếu maxAllowedWidth >= originalMaxWidth, không cần giới hạn
|
|
739
|
+
}
|
|
740
|
+
return originalMaxWidth;
|
|
695
741
|
};
|
|
696
|
-
const
|
|
742
|
+
const drawMockup = (ctx, canvas, imageRefs) => {
|
|
743
|
+
const mockupBounds = getMockupBounds(canvas, imageRefs);
|
|
744
|
+
if (!mockupBounds)
|
|
745
|
+
return;
|
|
746
|
+
const mockupImg = imageRefs.current.get("mockup");
|
|
747
|
+
if (!mockupImg)
|
|
748
|
+
return;
|
|
749
|
+
ctx.drawImage(mockupImg, mockupBounds.x, mockupBounds.y, mockupBounds.width, mockupBounds.height);
|
|
750
|
+
};
|
|
751
|
+
const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
697
752
|
let currentY = startY;
|
|
698
753
|
const padding = LAYOUT.PADDING * scaleFactor;
|
|
699
754
|
const sideWidth = width - 2 * padding;
|
|
@@ -702,7 +757,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
702
757
|
const headerFontSize = LAYOUT.HEADER_FONT_SIZE * scaleFactor;
|
|
703
758
|
ctx.font = `bold ${headerFontSize}px ${LAYOUT.HEADER_FONT_FAMILY}`;
|
|
704
759
|
ctx.fillStyle = LAYOUT.HEADER_COLOR;
|
|
705
|
-
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
|
|
760
|
+
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize, mockupBounds);
|
|
706
761
|
// Draw underline
|
|
707
762
|
const underlineY = headerResult.lastLineY + headerFontSize * LAYOUT.UNDERLINE_POSITION;
|
|
708
763
|
ctx.strokeStyle = LAYOUT.HEADER_COLOR;
|
|
@@ -763,7 +818,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
763
818
|
: { color: true };
|
|
764
819
|
// Render uniform labels (when applicable)
|
|
765
820
|
if (shouldShowUniformLabels) {
|
|
766
|
-
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields);
|
|
821
|
+
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields, mockupBounds);
|
|
767
822
|
}
|
|
768
823
|
// Group text positions by common properties
|
|
769
824
|
const textGroups = groupTextPositions(textPositions);
|
|
@@ -789,7 +844,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
789
844
|
floral: !uniformProps.isUniform.floral,
|
|
790
845
|
color: !uniformProps.isUniform.color,
|
|
791
846
|
};
|
|
792
|
-
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs);
|
|
847
|
+
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs, mockupBounds);
|
|
793
848
|
if (height > 0) {
|
|
794
849
|
currentY += height + LAYOUT.PADDING * scaleFactor;
|
|
795
850
|
textCounter++;
|
|
@@ -810,7 +865,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
810
865
|
iconColorValue !== null &&
|
|
811
866
|
uniformProps.values.color === iconColorValue &&
|
|
812
867
|
iconUsesSingleColor;
|
|
813
|
-
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor });
|
|
868
|
+
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor }, mockupBounds);
|
|
814
869
|
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
815
870
|
}
|
|
816
871
|
});
|
|
@@ -881,7 +936,7 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
881
936
|
},
|
|
882
937
|
};
|
|
883
938
|
};
|
|
884
|
-
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields) => {
|
|
939
|
+
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields, mockupBounds = null) => {
|
|
885
940
|
const { values } = uniformProps;
|
|
886
941
|
const fontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
887
942
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -919,7 +974,7 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
919
974
|
rendered++;
|
|
920
975
|
}
|
|
921
976
|
if (values.shape && values.shape !== "None" && shouldRenderField("shape")) {
|
|
922
|
-
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
977
|
+
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap, mockupBounds);
|
|
923
978
|
cursorY += result.height;
|
|
924
979
|
rendered++;
|
|
925
980
|
}
|
|
@@ -929,14 +984,20 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
929
984
|
: [values.color];
|
|
930
985
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
931
986
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
932
|
-
//
|
|
933
|
-
const
|
|
987
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
988
|
+
const lineHeight = fontSize + lineGap;
|
|
989
|
+
const effectiveMaxWidth = mockupBounds
|
|
990
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
991
|
+
: maxWidth;
|
|
992
|
+
// Vẽ text với effectiveMaxWidth
|
|
993
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
934
994
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
995
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
935
996
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
936
997
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
937
998
|
const swatchesStartX = textEndX + spacing;
|
|
938
999
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
939
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1000
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
940
1001
|
let swatchX;
|
|
941
1002
|
let swatchY;
|
|
942
1003
|
if (shouldWrapSwatches) {
|
|
@@ -969,16 +1030,16 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
969
1030
|
}
|
|
970
1031
|
// Line height giống icon_image: floralH + lineGap
|
|
971
1032
|
const floralLineHeight = floralH + lineGap;
|
|
972
|
-
// Text align
|
|
973
|
-
const
|
|
1033
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1034
|
+
const textCenterY = cursorY + floralH / 2;
|
|
974
1035
|
// Đo width trước khi vẽ
|
|
975
1036
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
976
1037
|
const labelText = `Mẫu hoa: ${values.floral}`;
|
|
977
1038
|
const labelWidth = ctx.measureText(labelText).width;
|
|
978
|
-
// Vẽ text với textBaseline =
|
|
979
|
-
ctx.textBaseline = "
|
|
1039
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1040
|
+
ctx.textBaseline = "middle";
|
|
980
1041
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
981
|
-
ctx.fillText(labelText, x,
|
|
1042
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
982
1043
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
983
1044
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
984
1045
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -992,13 +1053,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
992
1053
|
if (shouldWrapFloral) {
|
|
993
1054
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
994
1055
|
floralX = x;
|
|
995
|
-
floralY =
|
|
1056
|
+
floralY = cursorY + floralH + lineGap;
|
|
996
1057
|
cursorY += floralLineHeight;
|
|
997
1058
|
}
|
|
998
1059
|
else {
|
|
999
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1060
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1000
1061
|
floralX = floralStartX;
|
|
1001
|
-
floralY =
|
|
1062
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1002
1063
|
cursorY += floralLineHeight;
|
|
1003
1064
|
}
|
|
1004
1065
|
// Vẽ ảnh floral
|
|
@@ -1013,19 +1074,17 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1013
1074
|
return cursorY - y;
|
|
1014
1075
|
};
|
|
1015
1076
|
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) => {
|
|
1077
|
+
displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
1017
1078
|
ctx.save();
|
|
1018
1079
|
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1019
1080
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1020
1081
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
1021
1082
|
let currentY = y;
|
|
1022
|
-
let drawnHeight = 0;
|
|
1023
1083
|
// Chuẩn hóa xuống dòng:
|
|
1024
1084
|
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1025
1085
|
const normalizeNewlines = (text) => text
|
|
1026
1086
|
.replace(/\r\n/g, "\n")
|
|
1027
|
-
.replace(/\r/g, "\n")
|
|
1028
|
-
.replace(/\\n/g, "\n");
|
|
1087
|
+
.replace(/\r/g, "\n");
|
|
1029
1088
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1030
1089
|
const rawOriginalText = position.text ?? "";
|
|
1031
1090
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1050,37 +1109,52 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1050
1109
|
const lines = rawText.split("\n");
|
|
1051
1110
|
// Tính font-size hiệu dụng cho phần value sao cho:
|
|
1052
1111
|
// - Không vượt quá availableWidth
|
|
1112
|
+
// - Tính đến mockup bounds cho từng dòng
|
|
1053
1113
|
// - 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
1114
|
let effectiveTextFontSize = textFontSize;
|
|
1065
1115
|
if (!isEmptyText) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1116
|
+
// Tính vị trí Y của từng dòng (textCenterY và lineY)
|
|
1117
|
+
const valueLineHeight = textFontSize;
|
|
1118
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1119
|
+
const textCenterY = currentY + textBlockHeight / 2;
|
|
1120
|
+
// Tính font size cần thiết cho từng dòng dựa trên effectiveMaxWidth của nó
|
|
1121
|
+
let minShrinkRatio = 1;
|
|
1122
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1123
|
+
lines.forEach((line, idx) => {
|
|
1124
|
+
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1125
|
+
// Tính effectiveMaxWidth cho dòng này
|
|
1126
|
+
const effectiveMaxWidth = mockupBounds
|
|
1127
|
+
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
1128
|
+
: availableWidth;
|
|
1129
|
+
const lineWidth = ctx.measureText(line).width;
|
|
1130
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1131
|
+
// Cần thu nhỏ font cho dòng này
|
|
1132
|
+
const shrinkRatio = effectiveMaxWidth / lineWidth;
|
|
1133
|
+
if (shrinkRatio < minShrinkRatio) {
|
|
1134
|
+
minShrinkRatio = shrinkRatio;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
effectiveTextFontSize = textFontSize * minShrinkRatio;
|
|
1071
1139
|
}
|
|
1072
|
-
//
|
|
1140
|
+
// Line height luôn theo Text label (textFontSize), không theo effectiveTextFontSize
|
|
1141
|
+
const valueLineHeight = textFontSize;
|
|
1142
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1143
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1144
|
+
const textCenterY = currentY + textBlockHeight / 2;
|
|
1145
|
+
// Vẽ phần value với font hiệu dụng, màu đỏ, align center
|
|
1146
|
+
ctx.textBaseline = "middle";
|
|
1073
1147
|
ctx.font = `${effectiveTextFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1074
1148
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1075
|
-
|
|
1076
|
-
|
|
1149
|
+
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1150
|
+
// Font size đã được tính để vừa với effectiveMaxWidth của từng dòng, nên không cần cắt text
|
|
1077
1151
|
lines.forEach((line, idx) => {
|
|
1078
|
-
|
|
1079
|
-
|
|
1152
|
+
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1153
|
+
ctx.fillText(line, valueStartX, lineY);
|
|
1080
1154
|
});
|
|
1081
|
-
|
|
1155
|
+
// Reset textBaseline về top cho các phần tiếp theo
|
|
1156
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1082
1157
|
currentY += textBlockHeight;
|
|
1083
|
-
drawnHeight += textBlockHeight;
|
|
1084
1158
|
// Draw additional labels (skip when text is empty)
|
|
1085
1159
|
if (!isEmptyText) {
|
|
1086
1160
|
currentY += lineGap;
|
|
@@ -1089,9 +1163,8 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1089
1163
|
// Lưu ý: phần dưới này vẫn có thể wrap theo maxWidth vì đây chỉ là label mô tả,
|
|
1090
1164
|
// không phải nội dung Text chính cần giữ nguyên format.
|
|
1091
1165
|
if (showLabels.shape && position.text_shape) {
|
|
1092
|
-
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
1166
|
+
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap, mockupBounds);
|
|
1093
1167
|
currentY += result.height;
|
|
1094
|
-
drawnHeight += result.height;
|
|
1095
1168
|
}
|
|
1096
1169
|
if (showLabels.font && position.font) {
|
|
1097
1170
|
// Render "Font: " với font mặc định
|
|
@@ -1113,7 +1186,6 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1113
1186
|
// Tính toán height và di chuyển cursorY
|
|
1114
1187
|
const lineHeight = otherFontSize + lineGap;
|
|
1115
1188
|
currentY += lineHeight;
|
|
1116
|
-
drawnHeight += lineHeight;
|
|
1117
1189
|
}
|
|
1118
1190
|
if (showLabels.color) {
|
|
1119
1191
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1121,14 +1193,20 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1121
1193
|
const colors = position.character_colors || [position.color];
|
|
1122
1194
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1123
1195
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
1124
|
-
//
|
|
1125
|
-
const
|
|
1196
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1197
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1198
|
+
const effectiveMaxWidth = mockupBounds
|
|
1199
|
+
? getEffectiveMaxWidth(x, currentY, lineHeight, maxWidth, mockupBounds)
|
|
1200
|
+
: maxWidth;
|
|
1201
|
+
// Vẽ text với effectiveMaxWidth
|
|
1202
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1126
1203
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1204
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
1127
1205
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
1128
1206
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1129
1207
|
const swatchesStartX = textEndX + spacing;
|
|
1130
1208
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1131
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1209
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1132
1210
|
let swatchX;
|
|
1133
1211
|
let swatchY;
|
|
1134
1212
|
if (shouldWrapSwatches) {
|
|
@@ -1136,19 +1214,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1136
1214
|
swatchX = x;
|
|
1137
1215
|
swatchY = result.lastLineY + otherFontSize + lineGap;
|
|
1138
1216
|
currentY += result.height + otherFontSize + lineGap;
|
|
1139
|
-
drawnHeight += result.height + otherFontSize + lineGap;
|
|
1140
1217
|
}
|
|
1141
1218
|
else {
|
|
1142
1219
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1143
1220
|
swatchX = swatchesStartX;
|
|
1144
1221
|
swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1145
1222
|
currentY += result.height;
|
|
1146
|
-
drawnHeight += result.height;
|
|
1147
1223
|
}
|
|
1148
1224
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1149
1225
|
if (shouldWrapSwatches) {
|
|
1150
1226
|
currentY += swatchH;
|
|
1151
|
-
drawnHeight += swatchH;
|
|
1152
1227
|
}
|
|
1153
1228
|
}
|
|
1154
1229
|
}
|
|
@@ -1164,16 +1239,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1164
1239
|
}
|
|
1165
1240
|
// Line height giống icon_image: floralH + lineGap
|
|
1166
1241
|
const floralLineHeight = floralH + lineGap;
|
|
1167
|
-
// Text align
|
|
1168
|
-
const
|
|
1242
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1243
|
+
const textCenterY = currentY + floralH / 2;
|
|
1169
1244
|
// Đo width trước khi vẽ
|
|
1170
1245
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1171
1246
|
const labelText = `Mẫu hoa: ${position.floral_pattern}`;
|
|
1172
1247
|
const labelWidth = ctx.measureText(labelText).width;
|
|
1173
|
-
// Vẽ text với textBaseline =
|
|
1174
|
-
ctx.textBaseline = "
|
|
1248
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1249
|
+
ctx.textBaseline = "middle";
|
|
1175
1250
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1176
|
-
ctx.fillText(labelText, x,
|
|
1251
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
1177
1252
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1178
1253
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1179
1254
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -1187,16 +1262,14 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1187
1262
|
if (shouldWrapFloral) {
|
|
1188
1263
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
1189
1264
|
floralX = x;
|
|
1190
|
-
floralY =
|
|
1265
|
+
floralY = currentY + floralH + lineGap;
|
|
1191
1266
|
currentY += floralLineHeight;
|
|
1192
|
-
drawnHeight += floralLineHeight;
|
|
1193
1267
|
}
|
|
1194
1268
|
else {
|
|
1195
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1269
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1196
1270
|
floralX = floralStartX;
|
|
1197
|
-
floralY =
|
|
1271
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1198
1272
|
currentY += floralLineHeight;
|
|
1199
|
-
drawnHeight += floralLineHeight;
|
|
1200
1273
|
}
|
|
1201
1274
|
// Vẽ ảnh floral
|
|
1202
1275
|
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
@@ -1205,9 +1278,10 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1205
1278
|
}
|
|
1206
1279
|
}
|
|
1207
1280
|
ctx.restore();
|
|
1208
|
-
|
|
1281
|
+
// Trả về toàn bộ chiều cao đã sử dụng trong block này
|
|
1282
|
+
return currentY - y;
|
|
1209
1283
|
};
|
|
1210
|
-
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
1284
|
+
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options, mockupBounds = null) => {
|
|
1211
1285
|
// Dùng cùng font size với Text cho label và value icon
|
|
1212
1286
|
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1213
1287
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -1239,30 +1313,148 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1239
1313
|
// Kiểm tra xem có icon_image không để tính height phù hợp
|
|
1240
1314
|
const hasIconImage = position.icon_image && position.icon_image.trim().length > 0;
|
|
1241
1315
|
const iconImageHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
|
|
1242
|
-
//
|
|
1243
|
-
const textBottomY = cursorY + iconImageHeight;
|
|
1244
|
-
// Đo width trước khi vẽ
|
|
1316
|
+
// Đo width của label
|
|
1245
1317
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1246
1318
|
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1319
|
+
// Icon value không cần chuẩn hóa xuống dòng, xử lý như một dòng text đơn giản
|
|
1320
|
+
// Tính kích thước icon_image nếu có để chừa khoảng trống
|
|
1321
|
+
let iconImageReservedWidth = 0;
|
|
1322
|
+
if (hasIconImage) {
|
|
1323
|
+
const iconUrl = getIconImageUrl(position);
|
|
1324
|
+
if (iconUrl) {
|
|
1325
|
+
const img = imageRefs.current.get(iconUrl);
|
|
1326
|
+
if (img?.complete && img.naturalHeight > 0) {
|
|
1327
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1328
|
+
const iconHeight = iconFontSize * 2;
|
|
1329
|
+
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1330
|
+
iconImageReservedWidth = iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// Tính available width cho icon value (trừ đi khoảng trống cho icon_image)
|
|
1335
|
+
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)
|
|
1338
|
+
const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
|
|
1339
|
+
const measureIconValueWidth = (fontSize) => {
|
|
1340
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1341
|
+
return ctx.measureText(` ${iconValue}`).width;
|
|
1342
|
+
};
|
|
1343
|
+
let effectiveIconValueFontSize = iconFontSize;
|
|
1344
|
+
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1345
|
+
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% có 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
|
|
1353
|
+
needsWrap = true;
|
|
1354
|
+
effectiveIconValueFontSize = MIN_ICON_VALUE_FONT_SIZE;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// Tính line height và 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
|
|
1374
|
+
ctx.textBaseline = "middle";
|
|
1252
1375
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1253
1376
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1254
|
-
ctx.fillText(iconLabel, x,
|
|
1255
|
-
|
|
1377
|
+
ctx.fillText(iconLabel, x, textCenterY);
|
|
1378
|
+
// Vẽ icon value với align center
|
|
1379
|
+
ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1256
1380
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1257
|
-
|
|
1381
|
+
let maxValueLineWidth = 0;
|
|
1382
|
+
// Vẽ icon value, căn giữa theo chiều dọc
|
|
1383
|
+
if (needsWrap) {
|
|
1384
|
+
// Có 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;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
ctx.fillText(line, valueStartX, lineY);
|
|
1411
|
+
const w = ctx.measureText(line).width;
|
|
1412
|
+
if (w > maxValueLineWidth)
|
|
1413
|
+
maxValueLineWidth = w;
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
// Chỉ một dòng, căn giữa
|
|
1419
|
+
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;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
ctx.fillText(lineText, valueStartX, textCenterY);
|
|
1444
|
+
const w = ctx.measureText(lineText).width;
|
|
1445
|
+
if (w > maxValueLineWidth)
|
|
1446
|
+
maxValueLineWidth = w;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1258
1449
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1259
1450
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1260
1451
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1261
1452
|
const iconResult = {
|
|
1262
1453
|
height: iconImageHeight + lineGap,
|
|
1263
1454
|
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1264
|
-
lastLineWidth: labelWidth +
|
|
1265
|
-
//
|
|
1455
|
+
lastLineWidth: labelWidth + maxValueLineWidth};
|
|
1456
|
+
// Kiểm tra xem phần icon image có vượt quá canvas không trước khi render
|
|
1457
|
+
// Draw icon image (không tự cắt theo canvasHeight, việc đảm bảo fit do scaleFactor xử lý)
|
|
1266
1458
|
const iconUrl = getIconImageUrl(position);
|
|
1267
1459
|
if (iconUrl) {
|
|
1268
1460
|
const img = imageRefs.current.get(iconUrl);
|
|
@@ -1274,7 +1466,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1274
1466
|
const iconX = x +
|
|
1275
1467
|
Math.ceil(iconResult.lastLineWidth) +
|
|
1276
1468
|
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1277
|
-
const iconY =
|
|
1469
|
+
const iconY = textCenterY - iconHeight / 2; // Align center với text
|
|
1278
1470
|
ctx.drawImage(img, iconX, iconY, swatchW, iconHeight);
|
|
1279
1471
|
}
|
|
1280
1472
|
}
|
|
@@ -1296,34 +1488,51 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1296
1488
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1297
1489
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1298
1490
|
const totalSwatchWidth = calculateSwatchesWidth(iconColors, swatchH, scaleFactor, imageRefs);
|
|
1299
|
-
//
|
|
1491
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1492
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1493
|
+
const effectiveMaxWidth = mockupBounds
|
|
1494
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
1495
|
+
: maxWidth;
|
|
1496
|
+
// Tính toán trước để kiểm tra xem có đủ chỗ không
|
|
1497
|
+
// Đo chiều cao của text "Màu chỉ:" trước
|
|
1300
1498
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1499
|
+
const testText = `Màu chỉ: ${iconColors.join(", ")}`;
|
|
1500
|
+
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1501
|
+
testLines.length * lineHeight;
|
|
1304
1502
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1305
|
-
const
|
|
1503
|
+
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1504
|
+
const textEndX = x + testTextWidth;
|
|
1306
1505
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1307
1506
|
const swatchesStartX = textEndX + spacing;
|
|
1308
1507
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1309
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1508
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1509
|
+
if (shouldWrapSwatches) ;
|
|
1510
|
+
else {
|
|
1511
|
+
// Swatches trên cùng dòng với text
|
|
1512
|
+
cursorY + (testLines.length - 1) * lineHeight;
|
|
1513
|
+
}
|
|
1514
|
+
// Luôn render text và swatches; đảm bảo fit bằng scaleFactor ở tầng trên
|
|
1515
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1516
|
+
const colorResult = wrapText(ctx, testText, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1517
|
+
// Cập nhật cursorY sau khi render text
|
|
1518
|
+
cursorY += colorResult.height;
|
|
1310
1519
|
let swatchX;
|
|
1311
1520
|
let swatchY;
|
|
1312
1521
|
if (shouldWrapSwatches) {
|
|
1313
1522
|
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
1314
1523
|
swatchX = x;
|
|
1315
|
-
swatchY =
|
|
1316
|
-
|
|
1524
|
+
swatchY = cursorY;
|
|
1525
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1526
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1527
|
+
cursorY = swatchY + swatchH;
|
|
1317
1528
|
}
|
|
1318
1529
|
else {
|
|
1319
1530
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1320
1531
|
swatchX = swatchesStartX;
|
|
1321
1532
|
swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (shouldWrapSwatches) {
|
|
1326
|
-
cursorY += swatchH;
|
|
1533
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1534
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1535
|
+
// cursorY đã được cập nhật ở trên
|
|
1327
1536
|
}
|
|
1328
1537
|
}
|
|
1329
1538
|
ctx.restore();
|