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