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.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,23 +567,48 @@ 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 - LAYOUT.PADDING;
|
|
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, không cộng padding ở đây
|
|
582
|
+
let testMeasureY = 0;
|
|
583
|
+
if (config.warning_message) {
|
|
584
|
+
const testWarningHeight = renderWarning(measureCtx, measureCanvas, config.warning_message, testScale);
|
|
585
|
+
testMeasureY += testWarningHeight;
|
|
586
|
+
}
|
|
587
|
+
if (config.message) {
|
|
588
|
+
const testMessageHeight = renderWarning(measureCtx, measureCanvas, config.message, testScale, 0, "", 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 (không gồm padding)
|
|
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
|
-
measureY += sideHeight + measureSpacing;
|
|
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);
|
|
585
612
|
// Render warning & message with scaleFactor and get actual heights
|
|
586
613
|
let actualWarningHeight = 0;
|
|
587
614
|
let actualMessageHeight = 0;
|
|
@@ -594,7 +621,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
594
621
|
);
|
|
595
622
|
}
|
|
596
623
|
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
597
|
-
let currentY = LAYOUT.PADDING
|
|
624
|
+
let currentY = LAYOUT.PADDING;
|
|
598
625
|
if (config.warning_message && actualWarningHeight > 0) {
|
|
599
626
|
currentY += actualWarningHeight;
|
|
600
627
|
}
|
|
@@ -602,7 +629,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
602
629
|
currentY += actualMessageHeight;
|
|
603
630
|
}
|
|
604
631
|
config.sides.forEach((side) => {
|
|
605
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
632
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, canvas.height, scaleFactor, imageRefs, mockupBounds);
|
|
606
633
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
607
634
|
});
|
|
608
635
|
};
|
|
@@ -680,10 +707,10 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
|
|
|
680
707
|
// Return the actual height of the warning (number of lines * lineHeight)
|
|
681
708
|
return lines.length * lineHeight;
|
|
682
709
|
};
|
|
683
|
-
const
|
|
710
|
+
const getMockupBounds = (canvas, imageRefs) => {
|
|
684
711
|
const mockupImg = imageRefs.current.get("mockup");
|
|
685
712
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
686
|
-
return;
|
|
713
|
+
return null;
|
|
687
714
|
const margin = LAYOUT.PADDING;
|
|
688
715
|
const maxWidth = Math.min(1800, canvas.width * 0.375);
|
|
689
716
|
const maxHeight = canvas.height * 0.375;
|
|
@@ -692,10 +719,38 @@ const drawMockupAndFlorals = (ctx, canvas, imageRefs) => {
|
|
|
692
719
|
const height = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
693
720
|
const x = canvas.width - margin - width;
|
|
694
721
|
const y = canvas.height - margin - height;
|
|
695
|
-
|
|
696
|
-
|
|
722
|
+
return { x, y, width, height };
|
|
723
|
+
};
|
|
724
|
+
// Helper function để tính maxWidth hiệu dụng dựa trên vị trí Y và mockupBounds
|
|
725
|
+
const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds) => {
|
|
726
|
+
if (!mockupBounds)
|
|
727
|
+
return originalMaxWidth;
|
|
728
|
+
// Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
|
|
729
|
+
const lineTopY = y;
|
|
730
|
+
const lineBottomY = y + lineHeight;
|
|
731
|
+
const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
|
|
732
|
+
if (overlapsY) {
|
|
733
|
+
// Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
|
|
734
|
+
const maxAllowedWidth = mockupBounds.x - x;
|
|
735
|
+
// Chỉ giới hạn nếu maxAllowedWidth > 0 và nhỏ hơn originalMaxWidth
|
|
736
|
+
if (maxAllowedWidth > 0 && maxAllowedWidth < originalMaxWidth) {
|
|
737
|
+
return maxAllowedWidth;
|
|
738
|
+
}
|
|
739
|
+
// Nếu x >= mockupBounds.x, text đã nằm sau mockup, không cần giới hạn
|
|
740
|
+
// Hoặc nếu maxAllowedWidth >= originalMaxWidth, không cần giới hạn
|
|
741
|
+
}
|
|
742
|
+
return originalMaxWidth;
|
|
697
743
|
};
|
|
698
|
-
const
|
|
744
|
+
const drawMockup = (ctx, canvas, imageRefs) => {
|
|
745
|
+
const mockupBounds = getMockupBounds(canvas, imageRefs);
|
|
746
|
+
if (!mockupBounds)
|
|
747
|
+
return;
|
|
748
|
+
const mockupImg = imageRefs.current.get("mockup");
|
|
749
|
+
if (!mockupImg)
|
|
750
|
+
return;
|
|
751
|
+
ctx.drawImage(mockupImg, mockupBounds.x, mockupBounds.y, mockupBounds.width, mockupBounds.height);
|
|
752
|
+
};
|
|
753
|
+
const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
699
754
|
let currentY = startY;
|
|
700
755
|
const padding = LAYOUT.PADDING * scaleFactor;
|
|
701
756
|
const sideWidth = width - 2 * padding;
|
|
@@ -704,7 +759,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
704
759
|
const headerFontSize = LAYOUT.HEADER_FONT_SIZE * scaleFactor;
|
|
705
760
|
ctx.font = `bold ${headerFontSize}px ${LAYOUT.HEADER_FONT_FAMILY}`;
|
|
706
761
|
ctx.fillStyle = LAYOUT.HEADER_COLOR;
|
|
707
|
-
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
|
|
762
|
+
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize, mockupBounds);
|
|
708
763
|
// Draw underline
|
|
709
764
|
const underlineY = headerResult.lastLineY + headerFontSize * LAYOUT.UNDERLINE_POSITION;
|
|
710
765
|
ctx.strokeStyle = LAYOUT.HEADER_COLOR;
|
|
@@ -765,7 +820,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
765
820
|
: { color: true };
|
|
766
821
|
// Render uniform labels (when applicable)
|
|
767
822
|
if (shouldShowUniformLabels) {
|
|
768
|
-
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields);
|
|
823
|
+
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs, textPositions, uniformLabelFields, mockupBounds);
|
|
769
824
|
}
|
|
770
825
|
// Group text positions by common properties
|
|
771
826
|
const textGroups = groupTextPositions(textPositions);
|
|
@@ -791,7 +846,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
791
846
|
floral: !uniformProps.isUniform.floral,
|
|
792
847
|
color: !uniformProps.isUniform.color,
|
|
793
848
|
};
|
|
794
|
-
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs);
|
|
849
|
+
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs, mockupBounds);
|
|
795
850
|
if (height > 0) {
|
|
796
851
|
currentY += height + LAYOUT.PADDING * scaleFactor;
|
|
797
852
|
textCounter++;
|
|
@@ -812,7 +867,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
812
867
|
iconColorValue !== null &&
|
|
813
868
|
uniformProps.values.color === iconColorValue &&
|
|
814
869
|
iconUsesSingleColor;
|
|
815
|
-
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor });
|
|
870
|
+
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs, { hideColor }, mockupBounds);
|
|
816
871
|
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
817
872
|
}
|
|
818
873
|
});
|
|
@@ -883,7 +938,7 @@ const computeUniformProperties = (textPositions, options) => {
|
|
|
883
938
|
},
|
|
884
939
|
};
|
|
885
940
|
};
|
|
886
|
-
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields) => {
|
|
941
|
+
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs, textPositions, fields, mockupBounds = null) => {
|
|
887
942
|
const { values } = uniformProps;
|
|
888
943
|
const fontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
889
944
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -921,7 +976,7 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
921
976
|
rendered++;
|
|
922
977
|
}
|
|
923
978
|
if (values.shape && values.shape !== "None" && shouldRenderField("shape")) {
|
|
924
|
-
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
979
|
+
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap, mockupBounds);
|
|
925
980
|
cursorY += result.height;
|
|
926
981
|
rendered++;
|
|
927
982
|
}
|
|
@@ -931,14 +986,20 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
931
986
|
: [values.color];
|
|
932
987
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
933
988
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
934
|
-
//
|
|
935
|
-
const
|
|
989
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
990
|
+
const lineHeight = fontSize + lineGap;
|
|
991
|
+
const effectiveMaxWidth = mockupBounds
|
|
992
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
993
|
+
: maxWidth;
|
|
994
|
+
// Vẽ text với effectiveMaxWidth
|
|
995
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
936
996
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
997
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
937
998
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
938
999
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
939
1000
|
const swatchesStartX = textEndX + spacing;
|
|
940
1001
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
941
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1002
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
942
1003
|
let swatchX;
|
|
943
1004
|
let swatchY;
|
|
944
1005
|
if (shouldWrapSwatches) {
|
|
@@ -971,16 +1032,16 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
971
1032
|
}
|
|
972
1033
|
// Line height giống icon_image: floralH + lineGap
|
|
973
1034
|
const floralLineHeight = floralH + lineGap;
|
|
974
|
-
// Text align
|
|
975
|
-
const
|
|
1035
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1036
|
+
const textCenterY = cursorY + floralH / 2;
|
|
976
1037
|
// Đo width trước khi vẽ
|
|
977
1038
|
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
978
1039
|
const labelText = `Mẫu hoa: ${values.floral}`;
|
|
979
1040
|
const labelWidth = ctx.measureText(labelText).width;
|
|
980
|
-
// Vẽ text với textBaseline =
|
|
981
|
-
ctx.textBaseline = "
|
|
1041
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1042
|
+
ctx.textBaseline = "middle";
|
|
982
1043
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
983
|
-
ctx.fillText(labelText, x,
|
|
1044
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
984
1045
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
985
1046
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
986
1047
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -994,13 +1055,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
994
1055
|
if (shouldWrapFloral) {
|
|
995
1056
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
996
1057
|
floralX = x;
|
|
997
|
-
floralY =
|
|
1058
|
+
floralY = cursorY + floralH + lineGap;
|
|
998
1059
|
cursorY += floralLineHeight;
|
|
999
1060
|
}
|
|
1000
1061
|
else {
|
|
1001
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1062
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1002
1063
|
floralX = floralStartX;
|
|
1003
|
-
floralY =
|
|
1064
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1004
1065
|
cursorY += floralLineHeight;
|
|
1005
1066
|
}
|
|
1006
1067
|
// Vẽ ảnh floral
|
|
@@ -1015,19 +1076,17 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
1015
1076
|
return cursorY - y;
|
|
1016
1077
|
};
|
|
1017
1078
|
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) => {
|
|
1079
|
+
displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
|
|
1019
1080
|
ctx.save();
|
|
1020
1081
|
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1021
1082
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1022
1083
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
1023
1084
|
let currentY = y;
|
|
1024
|
-
let drawnHeight = 0;
|
|
1025
1085
|
// Chuẩn hóa xuống dòng:
|
|
1026
1086
|
// - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
|
|
1027
1087
|
const normalizeNewlines = (text) => text
|
|
1028
1088
|
.replace(/\r\n/g, "\n")
|
|
1029
|
-
.replace(/\r/g, "\n")
|
|
1030
|
-
.replace(/\\n/g, "\n");
|
|
1089
|
+
.replace(/\r/g, "\n");
|
|
1031
1090
|
// Get display text (handle empty/null/undefined) sau khi normalize
|
|
1032
1091
|
const rawOriginalText = position.text ?? "";
|
|
1033
1092
|
const normalizedText = normalizeNewlines(rawOriginalText);
|
|
@@ -1052,37 +1111,52 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1052
1111
|
const lines = rawText.split("\n");
|
|
1053
1112
|
// Tính font-size hiệu dụng cho phần value sao cho:
|
|
1054
1113
|
// - Không vượt quá availableWidth
|
|
1114
|
+
// - Tính đến mockup bounds cho từng dòng
|
|
1055
1115
|
// - 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
1116
|
let effectiveTextFontSize = textFontSize;
|
|
1067
1117
|
if (!isEmptyText) {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1118
|
+
// Tính vị trí Y của từng dòng (textCenterY và lineY)
|
|
1119
|
+
const valueLineHeight = textFontSize;
|
|
1120
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1121
|
+
const textCenterY = currentY + textBlockHeight / 2;
|
|
1122
|
+
// Tính font size cần thiết cho từng dòng dựa trên effectiveMaxWidth của nó
|
|
1123
|
+
let minShrinkRatio = 1;
|
|
1124
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1125
|
+
lines.forEach((line, idx) => {
|
|
1126
|
+
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1127
|
+
// Tính effectiveMaxWidth cho dòng này
|
|
1128
|
+
const effectiveMaxWidth = mockupBounds
|
|
1129
|
+
? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
|
|
1130
|
+
: availableWidth;
|
|
1131
|
+
const lineWidth = ctx.measureText(line).width;
|
|
1132
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1133
|
+
// Cần thu nhỏ font cho dòng này
|
|
1134
|
+
const shrinkRatio = effectiveMaxWidth / lineWidth;
|
|
1135
|
+
if (shrinkRatio < minShrinkRatio) {
|
|
1136
|
+
minShrinkRatio = shrinkRatio;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
effectiveTextFontSize = textFontSize * minShrinkRatio;
|
|
1073
1141
|
}
|
|
1074
|
-
//
|
|
1142
|
+
// Line height luôn theo Text label (textFontSize), không theo effectiveTextFontSize
|
|
1143
|
+
const valueLineHeight = textFontSize;
|
|
1144
|
+
const textBlockHeight = lines.length * valueLineHeight;
|
|
1145
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1146
|
+
const textCenterY = currentY + textBlockHeight / 2;
|
|
1147
|
+
// Vẽ phần value với font hiệu dụng, màu đỏ, align center
|
|
1148
|
+
ctx.textBaseline = "middle";
|
|
1075
1149
|
ctx.font = `${effectiveTextFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1076
1150
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1077
|
-
|
|
1078
|
-
|
|
1151
|
+
// Vẽ từ trên xuống: căn giữa mỗi dòng
|
|
1152
|
+
// 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
|
|
1079
1153
|
lines.forEach((line, idx) => {
|
|
1080
|
-
|
|
1081
|
-
|
|
1154
|
+
const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
|
|
1155
|
+
ctx.fillText(line, valueStartX, lineY);
|
|
1082
1156
|
});
|
|
1083
|
-
|
|
1157
|
+
// Reset textBaseline về top cho các phần tiếp theo
|
|
1158
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1084
1159
|
currentY += textBlockHeight;
|
|
1085
|
-
drawnHeight += textBlockHeight;
|
|
1086
1160
|
// Draw additional labels (skip when text is empty)
|
|
1087
1161
|
if (!isEmptyText) {
|
|
1088
1162
|
currentY += lineGap;
|
|
@@ -1091,9 +1165,8 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1091
1165
|
// Lưu ý: phần dưới này vẫn có thể wrap theo maxWidth vì đây chỉ là label mô tả,
|
|
1092
1166
|
// không phải nội dung Text chính cần giữ nguyên format.
|
|
1093
1167
|
if (showLabels.shape && position.text_shape) {
|
|
1094
|
-
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
1168
|
+
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap, mockupBounds);
|
|
1095
1169
|
currentY += result.height;
|
|
1096
|
-
drawnHeight += result.height;
|
|
1097
1170
|
}
|
|
1098
1171
|
if (showLabels.font && position.font) {
|
|
1099
1172
|
// Render "Font: " với font mặc định
|
|
@@ -1115,7 +1188,6 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1115
1188
|
// Tính toán height và di chuyển cursorY
|
|
1116
1189
|
const lineHeight = otherFontSize + lineGap;
|
|
1117
1190
|
currentY += lineHeight;
|
|
1118
|
-
drawnHeight += lineHeight;
|
|
1119
1191
|
}
|
|
1120
1192
|
if (showLabels.color) {
|
|
1121
1193
|
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
@@ -1123,14 +1195,20 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1123
1195
|
const colors = position.character_colors || [position.color];
|
|
1124
1196
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1125
1197
|
const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
|
|
1126
|
-
//
|
|
1127
|
-
const
|
|
1198
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1199
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1200
|
+
const effectiveMaxWidth = mockupBounds
|
|
1201
|
+
? getEffectiveMaxWidth(x, currentY, lineHeight, maxWidth, mockupBounds)
|
|
1202
|
+
: maxWidth;
|
|
1203
|
+
// Vẽ text với effectiveMaxWidth
|
|
1204
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1128
1205
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1206
|
+
// Sử dụng effectiveMaxWidth thay vì maxWidth
|
|
1129
1207
|
const textEndX = x + Math.ceil(result.lastLineWidth);
|
|
1130
1208
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1131
1209
|
const swatchesStartX = textEndX + spacing;
|
|
1132
1210
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1133
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1211
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1134
1212
|
let swatchX;
|
|
1135
1213
|
let swatchY;
|
|
1136
1214
|
if (shouldWrapSwatches) {
|
|
@@ -1138,19 +1216,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1138
1216
|
swatchX = x;
|
|
1139
1217
|
swatchY = result.lastLineY + otherFontSize + lineGap;
|
|
1140
1218
|
currentY += result.height + otherFontSize + lineGap;
|
|
1141
|
-
drawnHeight += result.height + otherFontSize + lineGap;
|
|
1142
1219
|
}
|
|
1143
1220
|
else {
|
|
1144
1221
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1145
1222
|
swatchX = swatchesStartX;
|
|
1146
1223
|
swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1147
1224
|
currentY += result.height;
|
|
1148
|
-
drawnHeight += result.height;
|
|
1149
1225
|
}
|
|
1150
1226
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1151
1227
|
if (shouldWrapSwatches) {
|
|
1152
1228
|
currentY += swatchH;
|
|
1153
|
-
drawnHeight += swatchH;
|
|
1154
1229
|
}
|
|
1155
1230
|
}
|
|
1156
1231
|
}
|
|
@@ -1166,16 +1241,16 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1166
1241
|
}
|
|
1167
1242
|
// Line height giống icon_image: floralH + lineGap
|
|
1168
1243
|
const floralLineHeight = floralH + lineGap;
|
|
1169
|
-
// Text align
|
|
1170
|
-
const
|
|
1244
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1245
|
+
const textCenterY = currentY + floralH / 2;
|
|
1171
1246
|
// Đo width trước khi vẽ
|
|
1172
1247
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1173
1248
|
const labelText = `Mẫu hoa: ${position.floral_pattern}`;
|
|
1174
1249
|
const labelWidth = ctx.measureText(labelText).width;
|
|
1175
|
-
// Vẽ text với textBaseline =
|
|
1176
|
-
ctx.textBaseline = "
|
|
1250
|
+
// Vẽ text với textBaseline = middle để align center
|
|
1251
|
+
ctx.textBaseline = "middle";
|
|
1177
1252
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1178
|
-
ctx.fillText(labelText, x,
|
|
1253
|
+
ctx.fillText(labelText, x, textCenterY);
|
|
1179
1254
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1180
1255
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1181
1256
|
// Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
|
|
@@ -1189,16 +1264,14 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1189
1264
|
if (shouldWrapFloral) {
|
|
1190
1265
|
// Không đủ chỗ, cho ảnh floral xuống dòng mới
|
|
1191
1266
|
floralX = x;
|
|
1192
|
-
floralY =
|
|
1267
|
+
floralY = currentY + floralH + lineGap;
|
|
1193
1268
|
currentY += floralLineHeight;
|
|
1194
|
-
drawnHeight += floralLineHeight;
|
|
1195
1269
|
}
|
|
1196
1270
|
else {
|
|
1197
|
-
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
|
|
1271
|
+
// Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng, align center
|
|
1198
1272
|
floralX = floralStartX;
|
|
1199
|
-
floralY =
|
|
1273
|
+
floralY = textCenterY - floralH / 2; // Align center với text
|
|
1200
1274
|
currentY += floralLineHeight;
|
|
1201
|
-
drawnHeight += floralLineHeight;
|
|
1202
1275
|
}
|
|
1203
1276
|
// Vẽ ảnh floral
|
|
1204
1277
|
if (floralImg?.complete && floralImg.naturalHeight > 0) {
|
|
@@ -1207,9 +1280,10 @@ displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
|
1207
1280
|
}
|
|
1208
1281
|
}
|
|
1209
1282
|
ctx.restore();
|
|
1210
|
-
|
|
1283
|
+
// Trả về toàn bộ chiều cao đã sử dụng trong block này
|
|
1284
|
+
return currentY - y;
|
|
1211
1285
|
};
|
|
1212
|
-
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
1286
|
+
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options, mockupBounds = null) => {
|
|
1213
1287
|
// Dùng cùng font size với Text cho label và value icon
|
|
1214
1288
|
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
1215
1289
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -1241,30 +1315,148 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1241
1315
|
// Kiểm tra xem có icon_image không để tính height phù hợp
|
|
1242
1316
|
const hasIconImage = position.icon_image && position.icon_image.trim().length > 0;
|
|
1243
1317
|
const iconImageHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
|
|
1244
|
-
//
|
|
1245
|
-
const textBottomY = cursorY + iconImageHeight;
|
|
1246
|
-
// Đo width trước khi vẽ
|
|
1318
|
+
// Đo width của label
|
|
1247
1319
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1248
1320
|
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1321
|
+
// 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
|
|
1322
|
+
// Tính kích thước icon_image nếu có để chừa khoảng trống
|
|
1323
|
+
let iconImageReservedWidth = 0;
|
|
1324
|
+
if (hasIconImage) {
|
|
1325
|
+
const iconUrl = getIconImageUrl(position);
|
|
1326
|
+
if (iconUrl) {
|
|
1327
|
+
const img = imageRefs.current.get(iconUrl);
|
|
1328
|
+
if (img?.complete && img.naturalHeight > 0) {
|
|
1329
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1330
|
+
const iconHeight = iconFontSize * 2;
|
|
1331
|
+
const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
|
|
1332
|
+
iconImageReservedWidth = iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
// Tính available width cho icon value (trừ đi khoảng trống cho icon_image)
|
|
1337
|
+
const availableWidth = Math.max(1, maxWidth - labelWidth - iconImageReservedWidth);
|
|
1338
|
+
// Tính font-size hiệu dụng cho icon value
|
|
1339
|
+
// Giới hạn thu nhỏ tối đa 50% (tối thiểu = iconFontSize * 0.5)
|
|
1340
|
+
const MIN_ICON_VALUE_FONT_SIZE = iconFontSize * 0.5;
|
|
1341
|
+
const measureIconValueWidth = (fontSize) => {
|
|
1342
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1343
|
+
return ctx.measureText(` ${iconValue}`).width;
|
|
1344
|
+
};
|
|
1345
|
+
let effectiveIconValueFontSize = iconFontSize;
|
|
1346
|
+
const baseMaxWidth = measureIconValueWidth(iconFontSize);
|
|
1347
|
+
let needsWrap = false;
|
|
1348
|
+
if (baseMaxWidth > availableWidth) {
|
|
1349
|
+
const shrinkRatio = availableWidth / baseMaxWidth;
|
|
1350
|
+
effectiveIconValueFontSize = Math.max(MIN_ICON_VALUE_FONT_SIZE, iconFontSize * shrinkRatio);
|
|
1351
|
+
// Kiểm tra xem sau khi thu nhỏ đến 50% có vẫn overflow không
|
|
1352
|
+
const minMaxWidth = measureIconValueWidth(MIN_ICON_VALUE_FONT_SIZE);
|
|
1353
|
+
if (minMaxWidth > availableWidth) {
|
|
1354
|
+
// Vẫn overflow, cần dùng wrap text
|
|
1355
|
+
needsWrap = true;
|
|
1356
|
+
effectiveIconValueFontSize = MIN_ICON_VALUE_FONT_SIZE;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// Tính line height và block height cho icon value
|
|
1360
|
+
const valueLineHeight = effectiveIconValueFontSize;
|
|
1361
|
+
let allWrappedLines = [];
|
|
1362
|
+
// Text align center: căn giữa theo chiều dọc trong block
|
|
1363
|
+
const textCenterY = cursorY + iconImageHeight / 2;
|
|
1364
|
+
const valueStartX = x + labelWidth;
|
|
1365
|
+
if (needsWrap) {
|
|
1366
|
+
// Dùng wrap text logic
|
|
1367
|
+
const wrappedLines = buildWrappedLines(ctx, iconValue, availableWidth, valueStartX, textCenterY, valueLineHeight, mockupBounds);
|
|
1368
|
+
allWrappedLines = wrappedLines;
|
|
1369
|
+
wrappedLines.length * valueLineHeight;
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
// Không cần wrap, chỉ một dòng
|
|
1373
|
+
allWrappedLines = [iconValue];
|
|
1374
|
+
}
|
|
1375
|
+
// Vẽ label với textBaseline = middle để align center với value
|
|
1376
|
+
ctx.textBaseline = "middle";
|
|
1254
1377
|
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1255
1378
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1256
|
-
ctx.fillText(iconLabel, x,
|
|
1257
|
-
|
|
1379
|
+
ctx.fillText(iconLabel, x, textCenterY);
|
|
1380
|
+
// Vẽ icon value với align center
|
|
1381
|
+
ctx.font = `${effectiveIconValueFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1258
1382
|
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1259
|
-
|
|
1383
|
+
let maxValueLineWidth = 0;
|
|
1384
|
+
// Vẽ icon value, căn giữa theo chiều dọc
|
|
1385
|
+
if (needsWrap) {
|
|
1386
|
+
// Có nhiều dòng sau khi wrap
|
|
1387
|
+
allWrappedLines.forEach((line, index) => {
|
|
1388
|
+
const lineY = textCenterY - (allWrappedLines.length - 1) / 2 * valueLineHeight + index * valueLineHeight;
|
|
1389
|
+
// Kiểm tra overlap với mockup và điều chỉnh text nếu cần
|
|
1390
|
+
if (mockupBounds) {
|
|
1391
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds);
|
|
1392
|
+
const lineWidth = ctx.measureText(line).width;
|
|
1393
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1394
|
+
// Cắt text cho đến khi vừa với effectiveMaxWidth
|
|
1395
|
+
let truncatedLine = line;
|
|
1396
|
+
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1397
|
+
truncatedLine = truncatedLine.slice(0, -1);
|
|
1398
|
+
}
|
|
1399
|
+
ctx.fillText(truncatedLine, valueStartX, lineY);
|
|
1400
|
+
const w = ctx.measureText(truncatedLine).width;
|
|
1401
|
+
if (w > maxValueLineWidth)
|
|
1402
|
+
maxValueLineWidth = w;
|
|
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
|
+
else {
|
|
1412
|
+
ctx.fillText(line, valueStartX, lineY);
|
|
1413
|
+
const w = ctx.measureText(line).width;
|
|
1414
|
+
if (w > maxValueLineWidth)
|
|
1415
|
+
maxValueLineWidth = w;
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
// Chỉ một dòng, căn giữa
|
|
1421
|
+
const lineText = ` ${iconValue}`;
|
|
1422
|
+
// Kiểm tra overlap với mockup và điều chỉnh text nếu cần
|
|
1423
|
+
if (mockupBounds) {
|
|
1424
|
+
const effectiveMaxWidth = getEffectiveMaxWidth(valueStartX, textCenterY, valueLineHeight, availableWidth, mockupBounds);
|
|
1425
|
+
const lineWidth = ctx.measureText(lineText).width;
|
|
1426
|
+
if (lineWidth > effectiveMaxWidth) {
|
|
1427
|
+
// Cắt text cho đến khi vừa với effectiveMaxWidth
|
|
1428
|
+
let truncatedLine = lineText;
|
|
1429
|
+
while (ctx.measureText(truncatedLine).width > effectiveMaxWidth && truncatedLine.length > 0) {
|
|
1430
|
+
truncatedLine = truncatedLine.slice(0, -1);
|
|
1431
|
+
}
|
|
1432
|
+
ctx.fillText(truncatedLine, valueStartX, textCenterY);
|
|
1433
|
+
const w = ctx.measureText(truncatedLine).width;
|
|
1434
|
+
if (w > maxValueLineWidth)
|
|
1435
|
+
maxValueLineWidth = w;
|
|
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
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
ctx.fillText(lineText, valueStartX, textCenterY);
|
|
1446
|
+
const w = ctx.measureText(lineText).width;
|
|
1447
|
+
if (w > maxValueLineWidth)
|
|
1448
|
+
maxValueLineWidth = w;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1260
1451
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1261
1452
|
// Reset textBaseline về top cho các phần tiếp theo
|
|
1262
1453
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
1263
1454
|
const iconResult = {
|
|
1264
1455
|
height: iconImageHeight + lineGap,
|
|
1265
1456
|
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1266
|
-
lastLineWidth: labelWidth +
|
|
1267
|
-
//
|
|
1457
|
+
lastLineWidth: labelWidth + maxValueLineWidth};
|
|
1458
|
+
// Kiểm tra xem phần icon image có vượt quá canvas không trước khi render
|
|
1459
|
+
// Draw icon image (không tự cắt theo canvasHeight, việc đảm bảo fit do scaleFactor xử lý)
|
|
1268
1460
|
const iconUrl = getIconImageUrl(position);
|
|
1269
1461
|
if (iconUrl) {
|
|
1270
1462
|
const img = imageRefs.current.get(iconUrl);
|
|
@@ -1276,7 +1468,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1276
1468
|
const iconX = x +
|
|
1277
1469
|
Math.ceil(iconResult.lastLineWidth) +
|
|
1278
1470
|
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1279
|
-
const iconY =
|
|
1471
|
+
const iconY = textCenterY - iconHeight / 2; // Align center với text
|
|
1280
1472
|
ctx.drawImage(img, iconX, iconY, swatchW, iconHeight);
|
|
1281
1473
|
}
|
|
1282
1474
|
}
|
|
@@ -1298,34 +1490,51 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
1298
1490
|
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
1299
1491
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
1300
1492
|
const totalSwatchWidth = calculateSwatchesWidth(iconColors, swatchH, scaleFactor, imageRefs);
|
|
1301
|
-
//
|
|
1493
|
+
// Tính effectiveMaxWidth cho dòng này để tránh mockup
|
|
1494
|
+
const lineHeight = otherFontSize + lineGap;
|
|
1495
|
+
const effectiveMaxWidth = mockupBounds
|
|
1496
|
+
? getEffectiveMaxWidth(x, cursorY, lineHeight, maxWidth, mockupBounds)
|
|
1497
|
+
: maxWidth;
|
|
1498
|
+
// Tính toán trước để kiểm tra xem có đủ chỗ không
|
|
1499
|
+
// Đo chiều cao của text "Màu chỉ:" trước
|
|
1302
1500
|
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1501
|
+
const testText = `Màu chỉ: ${iconColors.join(", ")}`;
|
|
1502
|
+
const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
|
|
1503
|
+
testLines.length * lineHeight;
|
|
1306
1504
|
// Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
|
|
1307
|
-
const
|
|
1505
|
+
const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
|
|
1506
|
+
const textEndX = x + testTextWidth;
|
|
1308
1507
|
const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
1309
1508
|
const swatchesStartX = textEndX + spacing;
|
|
1310
1509
|
const swatchesEndX = swatchesStartX + totalSwatchWidth;
|
|
1311
|
-
const shouldWrapSwatches = swatchesEndX > x +
|
|
1510
|
+
const shouldWrapSwatches = swatchesEndX > x + effectiveMaxWidth;
|
|
1511
|
+
if (shouldWrapSwatches) ;
|
|
1512
|
+
else {
|
|
1513
|
+
// Swatches trên cùng dòng với text
|
|
1514
|
+
cursorY + (testLines.length - 1) * lineHeight;
|
|
1515
|
+
}
|
|
1516
|
+
// Luôn render text và swatches; đảm bảo fit bằng scaleFactor ở tầng trên
|
|
1517
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1518
|
+
const colorResult = wrapText(ctx, testText, x, cursorY, effectiveMaxWidth, lineHeight, mockupBounds);
|
|
1519
|
+
// Cập nhật cursorY sau khi render text
|
|
1520
|
+
cursorY += colorResult.height;
|
|
1312
1521
|
let swatchX;
|
|
1313
1522
|
let swatchY;
|
|
1314
1523
|
if (shouldWrapSwatches) {
|
|
1315
1524
|
// Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
|
|
1316
1525
|
swatchX = x;
|
|
1317
|
-
swatchY =
|
|
1318
|
-
|
|
1526
|
+
swatchY = cursorY;
|
|
1527
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1528
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1529
|
+
cursorY = swatchY + swatchH;
|
|
1319
1530
|
}
|
|
1320
1531
|
else {
|
|
1321
1532
|
// Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
|
|
1322
1533
|
swatchX = swatchesStartX;
|
|
1323
1534
|
swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (shouldWrapSwatches) {
|
|
1328
|
-
cursorY += swatchH;
|
|
1535
|
+
// Render swatches (đã kiểm tra overflow ở trên)
|
|
1536
|
+
drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
1537
|
+
// cursorY đã được cập nhật ở trên
|
|
1329
1538
|
}
|
|
1330
1539
|
}
|
|
1331
1540
|
ctx.restore();
|