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.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,44 +565,63 @@ 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;
|
|
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, có bao gồm padding
|
|
580
|
+
let testMeasureY = LAYOUT.PADDING;
|
|
581
|
+
if (config.warning_message) {
|
|
582
|
+
const testWarningHeight = renderWarning(measureCtx, measureCanvas, config.warning_message, testScale, testMeasureY);
|
|
583
|
+
testMeasureY += testWarningHeight;
|
|
584
|
+
}
|
|
585
|
+
if (config.message) {
|
|
586
|
+
const testMessageHeight = renderWarning(measureCtx, measureCanvas, config.message, testScale, testMeasureY, "", 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
|
|
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
|
-
|
|
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);
|
|
610
|
+
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
611
|
+
let currentY = LAYOUT.PADDING;
|
|
583
612
|
// Render warning & message with scaleFactor and get actual heights
|
|
584
|
-
let actualWarningHeight = 0;
|
|
585
|
-
let actualMessageHeight = 0;
|
|
586
613
|
if (config.warning_message) {
|
|
587
|
-
actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
|
|
614
|
+
const actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor, currentY);
|
|
615
|
+
currentY += actualWarningHeight;
|
|
588
616
|
}
|
|
589
617
|
if (config.message) {
|
|
590
|
-
actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor,
|
|
618
|
+
const actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, currentY, "", // message: không cần prefix "Note"
|
|
591
619
|
DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
|
|
592
620
|
);
|
|
593
|
-
}
|
|
594
|
-
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
595
|
-
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
596
|
-
if (config.warning_message && actualWarningHeight > 0) {
|
|
597
|
-
currentY += actualWarningHeight;
|
|
598
|
-
}
|
|
599
|
-
if (config.message && actualMessageHeight > 0) {
|
|
600
621
|
currentY += actualMessageHeight;
|
|
601
622
|
}
|
|
602
623
|
config.sides.forEach((side) => {
|
|
603
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
624
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, canvas.height, scaleFactor, imageRefs, mockupBounds);
|
|
604
625
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
605
626
|
});
|
|
606
627
|
};
|
|
@@ -669,19 +690,18 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
|
|
|
669
690
|
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
670
691
|
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
671
692
|
}
|
|
672
|
-
const startY = LAYOUT.PADDING * scaleFactor + offsetY;
|
|
673
693
|
lines.forEach((line, index) => {
|
|
674
|
-
const y =
|
|
694
|
+
const y = offsetY + index * lineHeight;
|
|
675
695
|
ctx.fillText(line, leftX, y);
|
|
676
696
|
});
|
|
677
697
|
ctx.restore();
|
|
678
698
|
// Return the actual height of the warning (number of lines * lineHeight)
|
|
679
699
|
return lines.length * lineHeight;
|
|
680
700
|
};
|
|
681
|
-
const
|
|
701
|
+
const getMockupBounds = (canvas, imageRefs) => {
|
|
682
702
|
const mockupImg = imageRefs.current.get("mockup");
|
|
683
703
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
684
|
-
return;
|
|
704
|
+
return null;
|
|
685
705
|
const margin = LAYOUT.PADDING;
|
|
686
706
|
const maxWidth = Math.min(1800, canvas.width * 0.375);
|
|
687
707
|
const maxHeight = canvas.height * 0.375;
|
|
@@ -690,10 +710,38 @@ const drawMockupAndFlorals = (ctx, canvas, imageRefs) => {
|
|
|
690
710
|
const height = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
691
711
|
const x = canvas.width - margin - width;
|
|
692
712
|
const y = canvas.height - margin - height;
|
|
693
|
-
|
|
694
|
-
|
|
713
|
+
return { x, y, width, height };
|
|
714
|
+
};
|
|
715
|
+
// Helper function để tính maxWidth hiệu dụng dựa trên vị trí Y và mockupBounds
|
|
716
|
+
const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds) => {
|
|
717
|
+
if (!mockupBounds)
|
|
718
|
+
return originalMaxWidth;
|
|
719
|
+
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
720
|
+
const lineTopY = y;
|
|
721
|
+
const lineBottomY = y + lineHeight;
|
|
722
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
|
|
723
|
+
if (overlapsY) {
|
|
724
|
+
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
725
|
+
const maxAllowedWidth = mockupBounds.x - x;
|
|
726
|
+
// Chỉ giới hạn nếu maxAllowedWidth > 0 và nhỏ hơn originalMaxWidth
|
|
727
|
+
if (maxAllowedWidth > 0 && maxAllowedWidth < originalMaxWidth) {
|
|
728
|
+
return maxAllowedWidth;
|
|
729
|
+
}
|
|
730
|
+
// Nếu x >= mockupBounds.x, text đã nằm sau mockup, không cần giới hạn
|
|
731
|
+
// Hoặc nếu maxAllowedWidth >= originalMaxWidth, không cần giới hạn
|
|
732
|
+
}
|
|
733
|
+
return originalMaxWidth;
|
|
695
734
|
};
|
|
696
|
-
const
|
|
735
|
+
const drawMockup = (ctx, canvas, imageRefs) => {
|
|
736
|
+
const mockupBounds = getMockupBounds(canvas, imageRefs);
|
|
737
|
+
if (!mockupBounds)
|
|
738
|
+
return;
|
|
739
|
+
const mockupImg = imageRefs.current.get("mockup");
|
|
740
|
+
if (!mockupImg)
|
|
741
|
+
return;
|
|
742
|
+
ctx.drawImage(mockupImg, mockupBounds.x, mockupBounds.y, mockupBounds.width, mockupBounds.height);
|
|
743
|
+
};
|
|
744
|
+
const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
697
745
|
let currentY = startY;
|
|
698
746
|
const padding = LAYOUT.PADDING * scaleFactor;
|
|
699
747
|
const sideWidth = width - 2 * padding;
|
|
@@ -702,7 +750,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
702
750
|
const headerFontSize = LAYOUT.HEADER_FONT_SIZE * scaleFactor;
|
|
703
751
|
ctx.font = `bold ${headerFontSize}px ${LAYOUT.HEADER_FONT_FAMILY}`;
|
|
704
752
|
ctx.fillStyle = LAYOUT.HEADER_COLOR;
|
|
705
|
-
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
|
|
753
|
+
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize, mockupBounds);
|
|
706
754
|
// Draw underline
|
|
707
755
|
const underlineY = headerResult.lastLineY + headerFontSize * LAYOUT.UNDERLINE_POSITION;
|
|
708
756
|
ctx.strokeStyle = LAYOUT.HEADER_COLOR;
|
|
@@ -763,7 +811,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
763
811
|
: { color: true };
|
|
764
812
|
// Render uniform labels (when applicable)
|
|
765
813
|
if (shouldShowUniformLabels) {
|
|
766
|
-
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields);
|
|
814
|
+
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields, mockupBounds);
|
|
767
815
|
}
|
|
768
816
|
// Group text positions by common properties
|
|
769
817
|
const textGroups = groupTextPositions(textPositions);
|
|
@@ -789,7 +837,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
789
837
|
floral: !uniformProps.isUniform.floral,
|
|
790
838
|
color: !uniformProps.isUniform.color,
|
|
791
839
|
};
|
|
792
|
-
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs);
|
|
840
|
+
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs, mockupBounds);
|
|
793
841
|
if (height > 0) {
|
|
794
842
|
currentY += height + LAYOUT.PADDING * scaleFactor;
|
|
795
843
|
textCounter++;
|
|
@@ -810,7 +858,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
810
858
|
iconColorValue !== null &&
|
|
811
859
|
uniformProps.values.color === iconColorValue &&
|
|
812
860
|
iconUsesSingleColor;
|
|
813
|
-
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor });
|
|
861
|
+
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor }, mockupBounds);
|
|
814
862
|
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
815
863
|
}
|
|
816
864
|
});
|
|
@@ -881,7 +929,7 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
881
929
|
},
|
|
882
930
|
};
|
|
883
931
|
};
|
|
884
|
-
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields) => {
|
|
932
|
+
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields, mockupBounds = null) => {
|
|
885
933
|
const { values } = uniformProps;
|
|
886
934
|
const fontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
887
935
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -919,7 +967,7 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
919
967
|
rendered++;
|
|
920
968
|
}
|
|
921
969
|
if (values.shape && values.shape !== "None" && shouldRenderField("shape")) {
|
|
922
|
-
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
970
|
+
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap, mockupBounds);
|
|
923
971
|
cursorY += result.height;
|
|
924
972
|
rendered++;
|
|
925
973
|
}
|
|
@@ -929,14 +977,20 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
929
977
|
: [values.color];
|
|
930
978
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
931
979
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
932
|
-
//
|
|
933
|
-
const
|
|
980
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
981
|
+
const lineHeight = fontSize + lineGap;
|
|
982
|
+
const effectiveMaxWidth = mockupBounds
|
|
983
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
984
|
+
: maxWidth;
|
|
985
|
+
// Vẽ text với effectiveMaxWidth
|
|
986
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
934
987
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
988
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
935
989
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
936
990
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
937
991
|
const swatchesStartX = textEndX + spacing;
|
|
938
992
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
939
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
993
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
940
994
|
let swatchX;
|
|
941
995
|
let swatchY;
|
|
942
996
|
if (shouldWrapSwatches) {
|
|
@@ -969,16 +1023,16 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
969
1023
|
}
|
|
970
1024
|
// Line height giống icon_image: floralH + lineGap
|
|
971
1025
|
const floralLineHeight = floralH + lineGap;
|
|
972
|
-
// Text align
|
|
973
|
-
const
|
|
1026
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1027
|
+
const textCenterY = cursorY + floralH / 2;
|
|
974
1028
|
// Đo width trước khi vẽ
|
|
975
1029
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
976
1030
|
const labelText = `Mẫu hoa: ${values.floral}`;
|
|
977
1031
|
const labelWidth = ctx.measureText(labelText).width;
|
|
978
|
-
// Vẽ text với textBaseline =
|
|
979
|
-
ctx.textBaseline = "
|
|
1032
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1033
|
+
ctx.textBaseline = "middle";
|
|
980
1034
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
981
|
-
ctx.fillText(labelText, x,
|
|
1035
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
982
1036
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
983
1037
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
984
1038
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -992,13 +1046,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
992
1046
|
if (shouldWrapFloral) {
|
|
993
1047
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
994
1048
|
floralX = x;
|
|
995
|
-
floralY =
|
|
1049
|
+
floralY = cursorY + floralH + lineGap;
|
|
996
1050
|
cursorY += floralLineHeight;
|
|
997
1051
|
}
|
|
998
1052
|
else {
|
|
999
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1053
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1000
1054
|
floralX = floralStartX;
|
|
1001
|
-
floralY =
|
|
1055
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1002
1056
|
cursorY += floralLineHeight;
|
|
1003
1057
|
}
|
|
1004
1058
|
// Vẽ ảnh floral
|
|
@@ -1013,19 +1067,17 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1013
1067
|
return cursorY - y;
|
|
1014
1068
|
};
|
|
1015
1069
|
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) => {
|
|
1070
|
+
displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
1017
1071
|
ctx.save();
|
|
1018
1072
|
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1019
1073
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1020
1074
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
1021
1075
|
let currentY = y;
|
|
1022
|
-
let drawnHeight = 0;
|
|
1023
1076
|
// Chuẩn hóa xuống dòng:
|
|
1024
1077
|
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1025
1078
|
const normalizeNewlines = (text) => text
|
|
1026
1079
|
.replace(/\r\n/g, "\n")
|
|
1027
|
-
.replace(/\r/g, "\n")
|
|
1028
|
-
.replace(/\\n/g, "\n");
|
|
1080
|
+
.replace(/\r/g, "\n");
|
|
1029
1081
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1030
1082
|
const rawOriginalText = position.text ?? "";
|
|
1031
1083
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1050,24 +1102,33 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1050
1102
|
const lines = rawText.split("\n");
|
|
1051
1103
|
// Tính font-size hiệu dụng cho phần value sao cho:
|
|
1052
1104
|
// - Không vượt quá availableWidth
|
|
1105
|
+
// - Tính đến mockup bounds cho từng dòng
|
|
1053
1106
|
// - 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
1107
|
let effectiveTextFontSize = textFontSize;
|
|
1065
1108
|
if (!isEmptyText) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1109
|
+
// Tính vị trí Y của từng dòng (textCenterY và lineY)
|
|
1110
|
+
const valueLineHeight = textFontSize;
|
|
1111
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1112
|
+
const textCenterY = currentY + textBlockHeight / 2;
|
|
1113
|
+
// Tính font size cần thiết cho từng dòng dựa trên effectiveMaxWidth của nó
|
|
1114
|
+
let minShrinkRatio = 1;
|
|
1115
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1116
|
+
lines.forEach((line, idx) => {
|
|
1117
|
+
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1118
|
+
// Tính effectiveMaxWidth cho dòng này
|
|
1119
|
+
const effectiveMaxWidth = mockupBounds
|
|
1120
|
+
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
1121
|
+
: availableWidth;
|
|
1122
|
+
const lineWidth = ctx.measureText(line).width;
|
|
1123
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1124
|
+
// Cần thu nhỏ font cho dòng này
|
|
1125
|
+
const shrinkRatio = effectiveMaxWidth / lineWidth;
|
|
1126
|
+
if (shrinkRatio < minShrinkRatio) {
|
|
1127
|
+
minShrinkRatio = shrinkRatio;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
effectiveTextFontSize = textFontSize * minShrinkRatio;
|
|
1071
1132
|
}
|
|
1072
1133
|
// Line height luôn theo Text label (textFontSize), không theo effectiveTextFontSize
|
|
1073
1134
|
const valueLineHeight = textFontSize;
|
|
@@ -1079,6 +1140,7 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1079
1140
|
ctx.font = `${effectiveTextFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1080
1141
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1081
1142
|
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1143
|
+
// 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
|
|
1082
1144
|
lines.forEach((line, idx) => {
|
|
1083
1145
|
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1084
1146
|
ctx.fillText(line, valueStartX, lineY);
|
|
@@ -1086,7 +1148,6 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1086
1148
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1087
1149
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1088
1150
|
currentY += textBlockHeight;
|
|
1089
|
-
drawnHeight += textBlockHeight;
|
|
1090
1151
|
// Draw additional labels (skip when text is empty)
|
|
1091
1152
|
if (!isEmptyText) {
|
|
1092
1153
|
currentY += lineGap;
|
|
@@ -1095,9 +1156,8 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1095
1156
|
// Lưu ý: phần dưới này vẫn có thể wrap theo maxWidth vì đây chỉ là label mô tả,
|
|
1096
1157
|
// không phải nội dung Text chính cần giữ nguyên format.
|
|
1097
1158
|
if (showLabels.shape && position.text_shape) {
|
|
1098
|
-
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
1159
|
+
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap, mockupBounds);
|
|
1099
1160
|
currentY += result.height;
|
|
1100
|
-
drawnHeight += result.height;
|
|
1101
1161
|
}
|
|
1102
1162
|
if (showLabels.font && position.font) {
|
|
1103
1163
|
// Render "Font: " với font mặc định
|
|
@@ -1119,7 +1179,6 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1119
1179
|
// Tính toán height và di chuyển cursorY
|
|
1120
1180
|
const lineHeight = otherFontSize + lineGap;
|
|
1121
1181
|
currentY += lineHeight;
|
|
1122
|
-
drawnHeight += lineHeight;
|
|
1123
1182
|
}
|
|
1124
1183
|
if (showLabels.color) {
|
|
1125
1184
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1127,14 +1186,20 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1127
1186
|
const colors = position.character_colors || [position.color];
|
|
1128
1187
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1129
1188
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
1130
|
-
//
|
|
1131
|
-
const
|
|
1189
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1190
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1191
|
+
const effectiveMaxWidth = mockupBounds
|
|
1192
|
+
? getEffectiveMaxWidth(x, currentY, lineHeight, maxWidth, mockupBounds)
|
|
1193
|
+
: maxWidth;
|
|
1194
|
+
// Vẽ text với effectiveMaxWidth
|
|
1195
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1132
1196
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1197
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
1133
1198
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
1134
1199
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1135
1200
|
const swatchesStartX = textEndX + spacing;
|
|
1136
1201
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1137
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1202
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1138
1203
|
let swatchX;
|
|
1139
1204
|
let swatchY;
|
|
1140
1205
|
if (shouldWrapSwatches) {
|
|
@@ -1142,19 +1207,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1142
1207
|
swatchX = x;
|
|
1143
1208
|
swatchY = result.lastLineY + otherFontSize + lineGap;
|
|
1144
1209
|
currentY += result.height + otherFontSize + lineGap;
|
|
1145
|
-
drawnHeight += result.height + otherFontSize + lineGap;
|
|
1146
1210
|
}
|
|
1147
1211
|
else {
|
|
1148
1212
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1149
1213
|
swatchX = swatchesStartX;
|
|
1150
1214
|
swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1151
1215
|
currentY += result.height;
|
|
1152
|
-
drawnHeight += result.height;
|
|
1153
1216
|
}
|
|
1154
1217
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1155
1218
|
if (shouldWrapSwatches) {
|
|
1156
1219
|
currentY += swatchH;
|
|
1157
|
-
drawnHeight += swatchH;
|
|
1158
1220
|
}
|
|
1159
1221
|
}
|
|
1160
1222
|
}
|
|
@@ -1170,16 +1232,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1170
1232
|
}
|
|
1171
1233
|
// Line height giống icon_image: floralH + lineGap
|
|
1172
1234
|
const floralLineHeight = floralH + lineGap;
|
|
1173
|
-
// Text align
|
|
1174
|
-
const
|
|
1235
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1236
|
+
const textCenterY = currentY + floralH / 2;
|
|
1175
1237
|
// Đo width trước khi vẽ
|
|
1176
1238
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1177
1239
|
const labelText = `Mẫu hoa: ${position.floral_pattern}`;
|
|
1178
1240
|
const labelWidth = ctx.measureText(labelText).width;
|
|
1179
|
-
// Vẽ text với textBaseline =
|
|
1180
|
-
ctx.textBaseline = "
|
|
1241
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1242
|
+
ctx.textBaseline = "middle";
|
|
1181
1243
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1182
|
-
ctx.fillText(labelText, x,
|
|
1244
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
1183
1245
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1184
1246
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1185
1247
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -1193,16 +1255,14 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1193
1255
|
if (shouldWrapFloral) {
|
|
1194
1256
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
1195
1257
|
floralX = x;
|
|
1196
|
-
floralY =
|
|
1258
|
+
floralY = currentY + floralH + lineGap;
|
|
1197
1259
|
currentY += floralLineHeight;
|
|
1198
|
-
drawnHeight += floralLineHeight;
|
|
1199
1260
|
}
|
|
1200
1261
|
else {
|
|
1201
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1262
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1202
1263
|
floralX = floralStartX;
|
|
1203
|
-
floralY =
|
|
1264
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1204
1265
|
currentY += floralLineHeight;
|
|
1205
|
-
drawnHeight += floralLineHeight;
|
|
1206
1266
|
}
|
|
1207
1267
|
// Vẽ ảnh floral
|
|
1208
1268
|
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
@@ -1211,9 +1271,10 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1211
1271
|
}
|
|
1212
1272
|
}
|
|
1213
1273
|
ctx.restore();
|
|
1214
|
-
|
|
1274
|
+
// Trả về toàn bộ chiều cao đã sử dụng trong block này
|
|
1275
|
+
return currentY - y;
|
|
1215
1276
|
};
|
|
1216
|
-
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
1277
|
+
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options, mockupBounds = null) => {
|
|
1217
1278
|
// Dùng cùng font size với Text cho label và value icon
|
|
1218
1279
|
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1219
1280
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -1248,13 +1309,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1248
1309
|
// Đo width của label
|
|
1249
1310
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1250
1311
|
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1251
|
-
//
|
|
1252
|
-
const normalizeNewlines = (text) => text
|
|
1253
|
-
.replace(/\r\n/g, "\n")
|
|
1254
|
-
.replace(/\r/g, "\n")
|
|
1255
|
-
.replace(/\\n/g, "\n");
|
|
1256
|
-
const normalizedIconValue = normalizeNewlines(iconValue);
|
|
1257
|
-
const iconValueLines = normalizedIconValue.split("\n");
|
|
1312
|
+
// 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
|
|
1258
1313
|
// Tính kích thước icon_image nếu có để chừa khoảng trống
|
|
1259
1314
|
let iconImageReservedWidth = 0;
|
|
1260
1315
|
if (hasIconImage) {
|
|
@@ -1274,24 +1329,18 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1274
1329
|
// Tính font-size hiệu dụng cho icon value
|
|
1275
1330
|
// Giới hạn thu nhỏ tối đa 50% (tối thiểu = iconFontSize * 0.5)
|
|
1276
1331
|
const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
|
|
1277
|
-
const
|
|
1332
|
+
const measureIconValueWidth = (fontSize) => {
|
|
1278
1333
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1279
|
-
|
|
1280
|
-
iconValueLines.forEach((line) => {
|
|
1281
|
-
const w = ctx.measureText(` ${line}`).width;
|
|
1282
|
-
if (w > maxLineWidth)
|
|
1283
|
-
maxLineWidth = w;
|
|
1284
|
-
});
|
|
1285
|
-
return maxLineWidth;
|
|
1334
|
+
return ctx.measureText(` ${iconValue}`).width;
|
|
1286
1335
|
};
|
|
1287
1336
|
let effectiveIconValueFontSize = iconFontSize;
|
|
1288
|
-
const baseMaxWidth =
|
|
1337
|
+
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1289
1338
|
let needsWrap = false;
|
|
1290
1339
|
if (baseMaxWidth > availableWidth) {
|
|
1291
1340
|
const shrinkRatio = availableWidth / baseMaxWidth;
|
|
1292
1341
|
effectiveIconValueFontSize = Math.max(MIN_ICON_VALUE_FONT_SIZE, iconFontSize * shrinkRatio);
|
|
1293
1342
|
// Kiểm tra xem sau khi thu nhỏ đến 50% có vẫn overflow không
|
|
1294
|
-
const minMaxWidth =
|
|
1343
|
+
const minMaxWidth = measureIconValueWidth(MIN_ICON_VALUE_FONT_SIZE);
|
|
1295
1344
|
if (minMaxWidth > availableWidth) {
|
|
1296
1345
|
// Vẫn overflow, cần dùng wrap text
|
|
1297
1346
|
needsWrap = true;
|
|
@@ -1301,39 +1350,95 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1301
1350
|
// Tính line height và block height cho icon value
|
|
1302
1351
|
const valueLineHeight = effectiveIconValueFontSize;
|
|
1303
1352
|
let allWrappedLines = [];
|
|
1353
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1354
|
+
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1355
|
+
const valueStartX = x + labelWidth;
|
|
1304
1356
|
if (needsWrap) {
|
|
1305
|
-
// Dùng wrap text logic
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
});
|
|
1310
|
-
allWrappedLines.length * valueLineHeight;
|
|
1357
|
+
// Dùng wrap text logic
|
|
1358
|
+
const wrappedLines = buildWrappedLines(ctx, iconValue, availableWidth, valueStartX, textCenterY, valueLineHeight, mockupBounds);
|
|
1359
|
+
allWrappedLines = wrappedLines;
|
|
1360
|
+
wrappedLines.length * valueLineHeight;
|
|
1311
1361
|
}
|
|
1312
1362
|
else {
|
|
1313
|
-
|
|
1314
|
-
|
|
1363
|
+
// Không cần wrap, chỉ một dòng
|
|
1364
|
+
allWrappedLines = [iconValue];
|
|
1315
1365
|
}
|
|
1316
|
-
// Text align center: căn giữa theo chiều dọc trong block
|
|
1317
|
-
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1318
1366
|
// Vẽ label với textBaseline = middle để align center với value
|
|
1319
1367
|
ctx.textBaseline = "middle";
|
|
1320
1368
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1321
1369
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1322
1370
|
ctx.fillText(iconLabel, x, textCenterY);
|
|
1323
|
-
const valueStartX = x + labelWidth;
|
|
1324
1371
|
// Vẽ icon value với align center
|
|
1325
1372
|
ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1326
1373
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1327
1374
|
let maxValueLineWidth = 0;
|
|
1328
|
-
// Vẽ
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1375
|
+
// Vẽ icon value, căn giữa theo chiều dọc
|
|
1376
|
+
if (needsWrap) {
|
|
1377
|
+
// Có nhiều dòng sau khi wrap
|
|
1378
|
+
allWrappedLines.forEach((line, index) => {
|
|
1379
|
+
const lineY = textCenterY - (allWrappedLines.length - 1) / 2 * valueLineHeight + index * valueLineHeight;
|
|
1380
|
+
// Kiểm tra overlap với mockup và điều chỉnh text nếu cần
|
|
1381
|
+
if (mockupBounds) {
|
|
1382
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds);
|
|
1383
|
+
const lineWidth = ctx.measureText(line).width;
|
|
1384
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1385
|
+
// Cắt text cho đến khi vừa với effectiveMaxWidth
|
|
1386
|
+
let truncatedLine = line;
|
|
1387
|
+
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1388
|
+
truncatedLine = truncatedLine.slice(0, -1);
|
|
1389
|
+
}
|
|
1390
|
+
ctx.fillText(truncatedLine, valueStartX, lineY);
|
|
1391
|
+
const w = ctx.measureText(truncatedLine).width;
|
|
1392
|
+
if (w > maxValueLineWidth)
|
|
1393
|
+
maxValueLineWidth = w;
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
ctx.fillText(line, valueStartX, lineY);
|
|
1397
|
+
const w = ctx.measureText(line).width;
|
|
1398
|
+
if (w > maxValueLineWidth)
|
|
1399
|
+
maxValueLineWidth = w;
|
|
1400
|
+
}
|
|
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
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
// Chỉ một dòng, căn giữa
|
|
1412
|
+
const lineText = ` ${iconValue}`;
|
|
1413
|
+
// Kiểm tra overlap với mockup và điều chỉnh text nếu cần
|
|
1414
|
+
if (mockupBounds) {
|
|
1415
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, textCenterY, valueLineHeight, availableWidth, mockupBounds);
|
|
1416
|
+
const lineWidth = ctx.measureText(lineText).width;
|
|
1417
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1418
|
+
// Cắt text cho đến khi vừa với effectiveMaxWidth
|
|
1419
|
+
let truncatedLine = lineText;
|
|
1420
|
+
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1421
|
+
truncatedLine = truncatedLine.slice(0, -1);
|
|
1422
|
+
}
|
|
1423
|
+
ctx.fillText(truncatedLine, valueStartX, textCenterY);
|
|
1424
|
+
const w = ctx.measureText(truncatedLine).width;
|
|
1425
|
+
if (w > maxValueLineWidth)
|
|
1426
|
+
maxValueLineWidth = w;
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
ctx.fillText(lineText, valueStartX, textCenterY);
|
|
1430
|
+
const w = ctx.measureText(lineText).width;
|
|
1431
|
+
if (w > maxValueLineWidth)
|
|
1432
|
+
maxValueLineWidth = w;
|
|
1433
|
+
}
|
|
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
|
+
}
|
|
1337
1442
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1338
1443
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1339
1444
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
@@ -1341,7 +1446,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1341
1446
|
height: iconImageHeight + lineGap,
|
|
1342
1447
|
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1343
1448
|
lastLineWidth: labelWidth + maxValueLineWidth};
|
|
1344
|
-
//
|
|
1449
|
+
// Kiểm tra xem phần icon image có vượt quá canvas không trước khi render
|
|
1450
|
+
// Draw icon image (không tự cắt theo canvasHeight, việc đảm bảo fit do scaleFactor xử lý)
|
|
1345
1451
|
const iconUrl = getIconImageUrl(position);
|
|
1346
1452
|
if (iconUrl) {
|
|
1347
1453
|
const img = imageRefs.current.get(iconUrl);
|
|
@@ -1375,34 +1481,51 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1375
1481
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1376
1482
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1377
1483
|
const totalSwatchWidth = calculateSwatchesWidth(iconColors, swatchH, scaleFactor, imageRefs);
|
|
1378
|
-
//
|
|
1484
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1485
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1486
|
+
const effectiveMaxWidth = mockupBounds
|
|
1487
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
1488
|
+
: maxWidth;
|
|
1489
|
+
// Tính toán trước để kiểm tra xem có đủ chỗ không
|
|
1490
|
+
// Đo chiều cao của text "Màu chỉ:" trước
|
|
1379
1491
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1492
|
+
const testText = `Màu chỉ: ${iconColors.join(", ")}`;
|
|
1493
|
+
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1494
|
+
testLines.length * lineHeight;
|
|
1383
1495
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1384
|
-
const
|
|
1496
|
+
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1497
|
+
const textEndX = x + testTextWidth;
|
|
1385
1498
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1386
1499
|
const swatchesStartX = textEndX + spacing;
|
|
1387
1500
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1388
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1501
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1502
|
+
if (shouldWrapSwatches) ;
|
|
1503
|
+
else {
|
|
1504
|
+
// Swatches trên cùng dòng với text
|
|
1505
|
+
cursorY + (testLines.length - 1) * lineHeight;
|
|
1506
|
+
}
|
|
1507
|
+
// Luôn render text và swatches; đảm bảo fit bằng scaleFactor ở tầng trên
|
|
1508
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1509
|
+
const colorResult = wrapText(ctx, testText, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1510
|
+
// Cập nhật cursorY sau khi render text
|
|
1511
|
+
cursorY += colorResult.height;
|
|
1389
1512
|
let swatchX;
|
|
1390
1513
|
let swatchY;
|
|
1391
1514
|
if (shouldWrapSwatches) {
|
|
1392
1515
|
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
1393
1516
|
swatchX = x;
|
|
1394
|
-
swatchY =
|
|
1395
|
-
|
|
1517
|
+
swatchY = cursorY;
|
|
1518
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1519
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1520
|
+
cursorY = swatchY + swatchH;
|
|
1396
1521
|
}
|
|
1397
1522
|
else {
|
|
1398
1523
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1399
1524
|
swatchX = swatchesStartX;
|
|
1400
1525
|
swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
if (shouldWrapSwatches) {
|
|
1405
|
-
cursorY += swatchH;
|
|
1526
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1527
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1528
|
+
// cursorY đã được cập nhật ở trên
|
|
1406
1529
|
}
|
|
1407
1530
|
}
|
|
1408
1531
|
ctx.restore();
|