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