embroidery-qc-image 1.0.30 → 1.0.31

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.
@@ -1 +1 @@
1
- {"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAofD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AA2kDF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwhBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAqkEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React$1 from 'react';
2
2
 
3
3
  interface TextPosition {
4
- type: 'TEXT';
4
+ type: "TEXT";
5
5
  text: string;
6
6
  text_shape?: string | null;
7
7
  color?: string | null;
@@ -9,9 +9,11 @@ interface TextPosition {
9
9
  is_font_default?: boolean | null;
10
10
  character_colors?: string[] | null;
11
11
  floral_pattern?: string | null;
12
+ layer_colors?: string[] | null;
13
+ attachment?: string | null;
12
14
  }
13
15
  interface IconPosition {
14
- type: 'ICON';
16
+ type: "ICON";
15
17
  icon: number;
16
18
  icon_name?: string | null;
17
19
  icon_image?: string | null;
@@ -24,6 +26,8 @@ type Position = TextPosition | IconPosition;
24
26
  interface Side {
25
27
  print_side: string;
26
28
  item_type?: string | null;
29
+ link?: string | null;
30
+ size?: string | null;
27
31
  positions: Position[];
28
32
  }
29
33
  interface EmbroideryQCConfig {
package/dist/index.esm.js CHANGED
@@ -31,6 +31,71 @@ function styleInject(css, ref) {
31
31
  var css_248z = ".render-embroidery {\r\n display: inline-block;\r\n position: relative;\r\n width: 100%;\r\n max-width: 100%;\r\n}\r\n\r\n.render-embroidery-canvas {\r\n display: block;\r\n width: 100%;\r\n height: auto;\r\n image-rendering: high-quality;\r\n background: transparent;\r\n}\r\n";
32
32
  styleInject(css_248z);
33
33
 
34
+ // ============================================================================
35
+ // CONSTANTS
36
+ // ============================================================================
37
+ const COLOR_MAP = {
38
+ "Army (1394)": "#545541",
39
+ Army: "#545541",
40
+ "Black (8)": "#060608",
41
+ Black: "#060608",
42
+ "Bubblegum (1309)": "#E77B9F",
43
+ Bubblegum: "#E77B9F",
44
+ "Carolina Blue (1274)": "#608CC9",
45
+ "Carolina Blue": "#608CC9",
46
+ "Celadon (1098)": "#8EAD8D",
47
+ Celadon: "#8EAD8D",
48
+ "Coffee Bean (1145)": "#502B23",
49
+ "Coffee Bean": "#502B23",
50
+ "Daffodil (1180)": "#FBE30D",
51
+ Daffodil: "#FBE30D",
52
+ "Dark Gray (1131)": "#2E272E",
53
+ "Dark Gray": "#2E272E",
54
+ "Doe Skin Beige (1344)": "#AE9B8B",
55
+ "Doe Skin Beige": "#AE9B8B",
56
+ "Dusty Blue (1373)": "#7B90A9",
57
+ "Dusty Blue": "#7B90A9",
58
+ "Forest Green (1397)": "#073020",
59
+ "Forest Green": "#073020",
60
+ "Gold (1425)": "#D2920A",
61
+ Gold: "#D2920A",
62
+ "Gray (1118)": "#9999A3",
63
+ Gray: "#9999A3",
64
+ "Ivory (1072)": "#E3DAC9",
65
+ Ivory: "#E3DAC9",
66
+ "Lavender (1032)": "#9274B6",
67
+ Lavender: "#9274B6",
68
+ "Light Denim (1133)": "#366696",
69
+ "Light Denim": "#366696",
70
+ "Light Salmon (1018)": "#E0A793",
71
+ "Light Salmon": "#E0A793",
72
+ "Maroon (1374)": "#480C1C",
73
+ Maroon: "#480C1C",
74
+ "Navy Blue (1044)": "#04072A",
75
+ "Navy Blue": "#04072A",
76
+ "Olive Green (1157)": "#625E1F",
77
+ "Olive Green": "#625E1F",
78
+ "Orange (1278)": "#D45D03",
79
+ Orange: "#D45D03",
80
+ "Peach Blush (1053)": "#E2C0B6",
81
+ "Peach Blush": "#E2C0B6",
82
+ "Pink (1148)": "#EFAFBF",
83
+ Pink: "#EFAFBF",
84
+ "Purple (1412)": "#37196F",
85
+ Purple: "#37196F",
86
+ "Red (1037)": "#9D000B",
87
+ Red: "#9D000B",
88
+ "Silver Sage (1396)": "#424F45",
89
+ "Silver Sage": "#424F45",
90
+ "Summer Sky (1432)": "#65A8D2",
91
+ "Summer Sky": "#65A8D2",
92
+ "Terra Cotta (1477)": "#AE3111",
93
+ "Terra Cotta": "#AE3111",
94
+ "Sand (1055)": "#D2C2AB",
95
+ Sand: "#D2C2AB",
96
+ "White (9)": "#D8D7DC",
97
+ White: "#D8D7DC",
98
+ };
34
99
  const DEFAULT_ERROR_COLOR = "#CC1F1A";
35
100
  const DEFAULT_WARNING_COLOR = "#FF8C00";
36
101
  const BASE_URLS = {
@@ -71,7 +136,7 @@ const loadFont = (fontName) => {
71
136
  // Check if font is already loaded in document.fonts (browser cache)
72
137
  const fontFaceSet = document.fonts;
73
138
  for (const font of fontFaceSet) {
74
- if (font.family === fontName && font.status === 'loaded') {
139
+ if (font.family === fontName && font.status === "loaded") {
75
140
  return Promise.resolve();
76
141
  }
77
142
  }
@@ -191,7 +256,9 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
191
256
  target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
192
257
  finalize();
193
258
  };
194
- const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
259
+ const desiredSrc = attemptedProxy
260
+ ? getProxyUrl(getResizeUrl(url))
261
+ : getResizeUrl(url);
195
262
  if (target.src !== desiredSrc) {
196
263
  target.src = desiredSrc;
197
264
  }
@@ -204,22 +271,22 @@ const getResizeUrl = (url) => {
204
271
  try {
205
272
  const urlObj = new URL(url);
206
273
  // Xử lý cdn.shopify.com
207
- if (urlObj.hostname === 'cdn.shopify.com') {
274
+ if (urlObj.hostname === "cdn.shopify.com") {
208
275
  // Set hoặc update query param width=400
209
- urlObj.searchParams.set('width', '400');
276
+ urlObj.searchParams.set("width", "400");
210
277
  return urlObj.toString();
211
278
  }
212
279
  // Xử lý m.media-amazon.com
213
- if (urlObj.hostname === 'm.media-amazon.com') {
280
+ if (urlObj.hostname === "m.media-amazon.com") {
214
281
  const pathname = urlObj.pathname;
215
282
  // Split pathname theo dấu /
216
- const pathArr = pathname.split('/');
283
+ const pathArr = pathname.split("/");
217
284
  // Lấy filename (phần cuối cùng)
218
285
  const filename = pathArr[pathArr.length - 1];
219
286
  // Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
220
- const cleanedFilename = filename.replace(/\._.*_/g, '');
287
+ const cleanedFilename = filename.replace(/\._.*_/g, "");
221
288
  // Split filename đã clean theo dấu .
222
- const parts = cleanedFilename.split('.');
289
+ const parts = cleanedFilename.split(".");
223
290
  if (parts.length >= 2) {
224
291
  // Lấy phần đầu và phần cuối
225
292
  const firstPart = parts[0];
@@ -229,16 +296,16 @@ const getResizeUrl = (url) => {
229
296
  // Thay filename mới vào pathArr
230
297
  pathArr[pathArr.length - 1] = newFilename;
231
298
  // Join lại
232
- urlObj.pathname = pathArr.join('/');
299
+ urlObj.pathname = pathArr.join("/");
233
300
  return urlObj.toString();
234
301
  }
235
302
  }
236
303
  // Xử lý i.etsystatic.com
237
- if (urlObj.hostname === 'i.etsystatic.com') {
304
+ if (urlObj.hostname === "i.etsystatic.com") {
238
305
  const pathname = urlObj.pathname;
239
306
  // Thay il_fullxfull bằng il_400x400
240
- if (pathname.includes('il_fullxfull')) {
241
- const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
307
+ if (pathname.includes("il_fullxfull")) {
308
+ const newPathname = pathname.replace(/il_fullxfull/g, "il_400x400");
242
309
  urlObj.pathname = newPathname;
243
310
  return urlObj.toString();
244
311
  }
@@ -362,7 +429,8 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight, mockupBounds = null) =>
362
429
  let lineToRender = line;
363
430
  if (ctx.measureText(line).width > effectiveMaxWidth) {
364
431
  // Cắt từng ký tự cho đến khi vừa
365
- while (ctx.measureText(lineToRender).width > effectiveMaxWidth && lineToRender.length > 0) {
432
+ while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
433
+ lineToRender.length > 0) {
366
434
  lineToRender = lineToRender.slice(0, -1);
367
435
  }
368
436
  }
@@ -533,6 +601,262 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
533
601
  // ============================================================================
534
602
  // RENDERING FUNCTIONS
535
603
  // ============================================================================
604
+ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
605
+ // Clear canvas
606
+ ctx.fillStyle = "#e7e7e7";
607
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
608
+ const padding = LAYOUT.PADDING * 6;
609
+ const usableWidth = canvas.width - padding * 2;
610
+ const usableHeight = canvas.height - padding * 2;
611
+ // Calculate sections
612
+ const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
613
+ const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
614
+ const topSectionY = padding;
615
+ const bottomSectionY = topSectionY + topSectionHeight;
616
+ // Get first side (stroke_patches should have only one side)
617
+ const side = config.sides[0];
618
+ if (!side || !side.positions.length) {
619
+ renderErrorState(ctx, canvas, "Không có dữ liệu positions");
620
+ return;
621
+ }
622
+ const position = side.positions[0];
623
+ if (position.type !== "TEXT") {
624
+ renderErrorState(ctx, canvas, "Position phải là TEXT");
625
+ return;
626
+ }
627
+ // Validate required fields
628
+ if (!position.font) {
629
+ renderErrorState(ctx, canvas, "Thiếu font");
630
+ return;
631
+ }
632
+ // Validate layer_colors
633
+ if (!position.layer_colors) {
634
+ renderErrorState(ctx, canvas, "Thiếu layer_colors");
635
+ return;
636
+ }
637
+ if (position.layer_colors.length !== 3) {
638
+ renderErrorState(ctx, canvas, `layer_colors phải có đúng 3 phần tử, hiện tại có ${position.layer_colors.length}`);
639
+ return;
640
+ }
641
+ // Validate từng màu
642
+ const [textColor, borderColor, backgroundColor] = position.layer_colors;
643
+ const missingColors = [];
644
+ if (!textColor || textColor.trim() === "") {
645
+ missingColors.push("Màu text (vị trí 1)");
646
+ }
647
+ if (!borderColor || borderColor.trim() === "") {
648
+ missingColors.push("Màu border (vị trí 2)");
649
+ }
650
+ if (!backgroundColor || backgroundColor.trim() === "") {
651
+ missingColors.push("Màu background (vị trí 3)");
652
+ }
653
+ if (missingColors.length > 0) {
654
+ renderErrorState(ctx, canvas, `Thiếu màu trong layer_colors:\n${missingColors.join("\n")}`);
655
+ return;
656
+ }
657
+ // ============================================================================
658
+ // TOP SECTION (2/3): Hiển thị mẫu preview
659
+ // ============================================================================
660
+ ctx.save();
661
+ // Draw "Hình mẫu:" label at the top
662
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
663
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
664
+ ctx.fillStyle = "#CC0000"; // Red color
665
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
666
+ // Adjust top section Y to account for title + extra spacing (40px)
667
+ const extraSpacing = 40;
668
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
669
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
670
+ // Calculate text size to fit top section
671
+ let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
672
+ ctx.font = `${previewFontSize}px ${position.font}`;
673
+ const text = position.text || "";
674
+ const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
675
+ const maxTextHeight = actualTopSectionHeight; // Use 60% of height
676
+ // Scale down font size to fit
677
+ let textWidth = ctx.measureText(text).width;
678
+ while (textWidth > maxTextWidth && previewFontSize > 50) {
679
+ previewFontSize *= 0.95;
680
+ ctx.font = `${previewFontSize}px ${position.font}`;
681
+ textWidth = ctx.measureText(text).width;
682
+ }
683
+ // Ensure text height also fits
684
+ while (previewFontSize > maxTextHeight && previewFontSize > 50) {
685
+ previewFontSize *= 0.95;
686
+ ctx.font = `${previewFontSize}px ${position.font}`;
687
+ }
688
+ // Update textWidth after final scaling
689
+ textWidth = ctx.measureText(text).width;
690
+ // Center the text in top section
691
+ const textX = padding + usableWidth / 2 - textWidth / 2;
692
+ const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
693
+ // Get color hex values
694
+ const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
695
+ const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
696
+ const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
697
+ // Calculate stroke widths
698
+ // Background needs to be MUCH thicker to create spacing from text
699
+ // Border needs to be even thicker to wrap around background
700
+ const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
701
+ const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
702
+ // Layer 1: Draw border stroke (outermost)
703
+ ctx.strokeStyle = borderColorHex;
704
+ ctx.lineWidth = borderWidth;
705
+ ctx.lineJoin = "round";
706
+ ctx.lineCap = "round";
707
+ ctx.strokeText(text, textX, textY);
708
+ // Layer 2: Draw background color stroke (middle - creates spacing from text)
709
+ ctx.strokeStyle = bgColorHex;
710
+ ctx.lineWidth = bgWidth;
711
+ ctx.lineJoin = "round";
712
+ ctx.lineCap = "round";
713
+ ctx.strokeText(text, textX, textY);
714
+ // Layer 3: Draw text fill ONLY (no stroke on text itself)
715
+ ctx.fillStyle = textColorHex;
716
+ ctx.fillText(text, textX, textY);
717
+ ctx.restore();
718
+ // ============================================================================
719
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
720
+ // ============================================================================
721
+ ctx.save();
722
+ // Draw border around bottom section for debugging
723
+ // ctx.strokeStyle = "#ccc";
724
+ // ctx.lineWidth = 2;
725
+ // ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
726
+ const bottomPadding = 0; // Không padding ngang, sát lề
727
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
728
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
729
+ // Split bottom section: 60% left for info, 40% right for image
730
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
731
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
732
+ const imageSectionX = padding + infoSectionWidth;
733
+ // Left side: Info list
734
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
735
+ const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
736
+ let infoY = bottomSectionY + LAYOUT.PADDING;
737
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
738
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
739
+ ctx.textAlign = "left";
740
+ ctx.textBaseline = "top";
741
+ // Asterisk prefix style
742
+ const drawAsterisk = (x, y) => {
743
+ ctx.save();
744
+ ctx.fillStyle = "#CC0000"; // Red asterisk
745
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
746
+ ctx.fillText("*", x, y);
747
+ ctx.restore();
748
+ };
749
+ const asteriskWidth = ctx.measureText("*").width + 5;
750
+ const startX = padding + asteriskWidth;
751
+ // Font - render "Font: " với font mặc định, tên font với font đó
752
+ drawAsterisk(padding, infoY);
753
+ const fontPrefix = "Font: ";
754
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
755
+ ctx.fillText(fontPrefix, startX, infoY);
756
+ // Render font name với chính font đó
757
+ const prefixWidth = ctx.measureText(fontPrefix).width;
758
+ ctx.font = `${infoFontSize}px ${position.font}`;
759
+ ctx.fillText(position.font, startX + prefixWidth, infoY);
760
+ infoY += infoLineHeight;
761
+ // Reset font về mặc định cho các dòng tiếp theo
762
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
763
+ // Màu chữ (Text Color)
764
+ drawAsterisk(padding, infoY);
765
+ const textColorLabel = `Màu chữ: ${textColor}`;
766
+ ctx.fillText(textColorLabel, startX, infoY);
767
+ // Draw text color swatch
768
+ const swatchSize = infoFontSize * 1.3; // Giảm từ 1.5 xuống 1.3
769
+ const swatchX = startX +
770
+ ctx.measureText(textColorLabel).width +
771
+ LAYOUT.ELEMENT_SPACING * 0.3; // Giảm spacing
772
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
773
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
774
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
775
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
776
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
777
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
778
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
779
+ }
780
+ infoY += infoLineHeight;
781
+ // Màu nền (Background Color)
782
+ drawAsterisk(padding + bottomPadding, infoY);
783
+ const bgColorLabel = `Màu nền: ${backgroundColor}`;
784
+ ctx.fillText(bgColorLabel, startX, infoY);
785
+ // Draw background color swatch
786
+ const bgSwatchX = startX + ctx.measureText(bgColorLabel).width + LAYOUT.ELEMENT_SPACING * 0.3;
787
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
788
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
789
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
790
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
791
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
792
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
793
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
794
+ }
795
+ infoY += infoLineHeight;
796
+ // Màu viền (Border Color)
797
+ drawAsterisk(padding + bottomPadding, infoY);
798
+ const borderColorLabel = `Màu viền: ${borderColor}`;
799
+ ctx.fillText(borderColorLabel, startX, infoY);
800
+ // Draw border color swatch
801
+ const borderSwatchX = startX +
802
+ ctx.measureText(borderColorLabel).width +
803
+ LAYOUT.ELEMENT_SPACING * 0.3;
804
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
805
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
806
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
807
+ if (borderColorSwatchImg?.complete &&
808
+ borderColorSwatchImg.naturalHeight > 0) {
809
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
810
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
811
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
812
+ }
813
+ infoY += infoLineHeight;
814
+ // Attachment
815
+ if (position.attachment) {
816
+ drawAsterisk(padding + bottomPadding, infoY);
817
+ const attachmentLabel = `Attachment: ${position.attachment}`;
818
+ ctx.fillText(attachmentLabel, startX, infoY);
819
+ infoY += infoLineHeight;
820
+ }
821
+ // Size
822
+ if (side.size) {
823
+ drawAsterisk(padding + bottomPadding, infoY);
824
+ const sizeLabel = `Size: ${side.size}`;
825
+ ctx.fillText(sizeLabel, startX, infoY);
826
+ infoY += infoLineHeight;
827
+ }
828
+ // Right side: Image from config.image_url
829
+ if (config.image_url) {
830
+ // Draw "Mockup" label
831
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
832
+ ctx.fillStyle = "#000000";
833
+ const mockupLabel = "Mockup";
834
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
835
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
836
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
837
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
838
+ const img = imageRefs.current.get(config.image_url) ??
839
+ imageRefs.current.get("mockup");
840
+ if (img?.complete && img.naturalWidth > 0) {
841
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
842
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
843
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
844
+ let drawWidth = maxImgWidth;
845
+ let drawHeight = drawWidth / imgAspectRatio;
846
+ if (drawHeight > maxImgHeight) {
847
+ drawHeight = maxImgHeight;
848
+ drawWidth = drawHeight * imgAspectRatio;
849
+ }
850
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
851
+ const imgY = bottomSectionY +
852
+ LAYOUT.PADDING +
853
+ mockupLabelHeight +
854
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
855
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
856
+ }
857
+ }
858
+ ctx.restore();
859
+ };
536
860
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
537
861
  const ctx = canvas.getContext("2d");
538
862
  if (!ctx)
@@ -547,6 +871,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
547
871
  }
548
872
  if (!config.sides?.length)
549
873
  return;
874
+ // Check if this is a stroke_patches layout
875
+ const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
876
+ if (hasStrokePatches) {
877
+ renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
878
+ return;
879
+ }
550
880
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
551
881
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
552
882
  if (config.image_url) {
@@ -719,7 +1049,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
719
1049
  // Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
720
1050
  const lineTopY = y;
721
1051
  const lineBottomY = y + lineHeight;
722
- const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
1052
+ const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
1053
+ lineBottomY > mockupBounds.y;
723
1054
  if (overlapsY) {
724
1055
  // Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
725
1056
  const maxAllowedWidth = mockupBounds.x - x;
@@ -766,15 +1097,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
766
1097
  const iconPositions = side.positions.filter((p) => p.type === "ICON");
767
1098
  // Kiểm tra tất cả TEXT positions có trống không
768
1099
  // Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
769
- const allTextEmpty = textPositions.length === 0 || textPositions.every((p) => {
770
- const text = p.text ?? "";
771
- return text.trim() === "";
772
- });
1100
+ const allTextEmpty = textPositions.length === 0 ||
1101
+ textPositions.every((p) => {
1102
+ const text = p.text ?? "";
1103
+ return text.trim() === "";
1104
+ });
773
1105
  // Kiểm tra tất cả ICON positions có is_delete_icon = true không
774
1106
  // Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
775
- const allIconsDeleted = iconPositions.length === 0 || iconPositions.every((p) => {
776
- return p.is_delete_icon === true;
777
- });
1107
+ const allIconsDeleted = iconPositions.length === 0 ||
1108
+ iconPositions.every((p) => {
1109
+ return p.is_delete_icon === true;
1110
+ });
778
1111
  // Nếu tất cả TEXT trống và tất cả ICON bị xóa, chỉ render dòng "(không thêu gì)"
779
1112
  if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
780
1113
  ctx.save();
@@ -788,7 +1121,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
788
1121
  return currentY - startY;
789
1122
  }
790
1123
  // Compute uniform properties
791
- const iconColorPositions = side.positions.filter((p) => p.type === "ICON" && (!p.layer_colors?.length || p.layer_colors.length === 1));
1124
+ const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
1125
+ (!p.layer_colors?.length || p.layer_colors.length === 1));
792
1126
  const iconColorValues = iconColorPositions
793
1127
  .map((p) => {
794
1128
  if (p.layer_colors?.length === 1)
@@ -910,7 +1244,8 @@ const computeUniformProperties = (textPositions, options) => {
910
1244
  ...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
911
1245
  ];
912
1246
  if (textPositions.length === 0 &&
913
- (!options?.additionalColorValues || options.additionalColorValues.length === 0)) {
1247
+ (!options?.additionalColorValues ||
1248
+ options.additionalColorValues.length === 0)) {
914
1249
  return defaults;
915
1250
  }
916
1251
  const colors = new Set(colorSources);
@@ -1011,7 +1346,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
1011
1346
  }
1012
1347
  rendered++;
1013
1348
  }
1014
- if (values.floral && values.floral !== "None" && shouldRenderField("floral")) {
1349
+ if (values.floral &&
1350
+ values.floral !== "None" &&
1351
+ shouldRenderField("floral")) {
1015
1352
  const floralUrl = getImageUrl("floral", values.floral);
1016
1353
  const floralImg = imageRefs.current.get(floralUrl);
1017
1354
  // Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
@@ -1075,9 +1412,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1075
1412
  let currentY = y;
1076
1413
  // Chuẩn hóa xuống dòng:
1077
1414
  // - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
1078
- const normalizeNewlines = (text) => text
1079
- .replace(/\r\n/g, "\n")
1080
- .replace(/\r/g, "\n");
1415
+ const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1081
1416
  // Get display text (handle empty/null/undefined) sau khi normalize
1082
1417
  const rawOriginalText = position.text ?? "";
1083
1418
  const normalizedText = normalizeNewlines(rawOriginalText);
@@ -1114,7 +1449,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1114
1449
  let minShrinkRatio = 1;
1115
1450
  ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
1116
1451
  lines.forEach((line, idx) => {
1117
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1452
+ const lineY = textCenterY -
1453
+ ((lines.length - 1) / 2) * valueLineHeight +
1454
+ idx * valueLineHeight;
1118
1455
  // Tính effectiveMaxWidth cho dòng này
1119
1456
  const effectiveMaxWidth = mockupBounds
1120
1457
  ? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
@@ -1142,7 +1479,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1142
1479
  // Vẽ từ trên xuống: căn giữa mỗi dòng
1143
1480
  // 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
1144
1481
  lines.forEach((line, idx) => {
1145
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1482
+ const lineY = textCenterY -
1483
+ ((lines.length - 1) / 2) * valueLineHeight +
1484
+ idx * valueLineHeight;
1146
1485
  ctx.fillText(line, valueStartX, lineY);
1147
1486
  });
1148
1487
  // Reset textBaseline về top cho các phần tiếp theo
@@ -1229,7 +1568,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1229
1568
  let lineToRender = line;
1230
1569
  const lineWidth = ctx.measureText(lineToRender).width;
1231
1570
  if (lineWidth > lineEffectiveMaxWidth) {
1232
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1571
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1572
+ lineToRender.length > 0) {
1233
1573
  lineToRender = lineToRender.slice(0, -1);
1234
1574
  }
1235
1575
  }
@@ -1254,7 +1594,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1254
1594
  if (totalWidth > shrunkEffectiveMaxWidth) {
1255
1595
  // Cần cắt font name
1256
1596
  let truncatedFontName = fontName;
1257
- while (ctx.measureText(truncatedFontName).width > shrunkEffectiveMaxWidth - prefixWidth && truncatedFontName.length > 0) {
1597
+ while (ctx.measureText(truncatedFontName).width >
1598
+ shrunkEffectiveMaxWidth - prefixWidth &&
1599
+ truncatedFontName.length > 0) {
1258
1600
  truncatedFontName = truncatedFontName.slice(0, -1);
1259
1601
  }
1260
1602
  ctx.fillText(truncatedFontName, currentX, currentY);
@@ -1268,7 +1610,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1268
1610
  const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
1269
1611
  if (remainingWidth > 0) {
1270
1612
  let truncatedSuffix = suffix;
1271
- while (ctx.measureText(truncatedSuffix).width > remainingWidth && truncatedSuffix.length > 0) {
1613
+ while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
1614
+ truncatedSuffix.length > 0) {
1272
1615
  truncatedSuffix = truncatedSuffix.slice(0, -1);
1273
1616
  }
1274
1617
  if (truncatedSuffix.length > 0) {
@@ -1309,7 +1652,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1309
1652
  else {
1310
1653
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1311
1654
  swatchX = swatchesStartX;
1312
- swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1655
+ swatchY =
1656
+ result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1313
1657
  currentY += result.height;
1314
1658
  }
1315
1659
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
@@ -1418,7 +1762,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1418
1762
  const ratio = img.naturalWidth / img.naturalHeight;
1419
1763
  const iconHeight = iconFontSize * 2;
1420
1764
  const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
1421
- iconImageReservedWidth = iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1765
+ iconImageReservedWidth =
1766
+ iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1422
1767
  }
1423
1768
  }
1424
1769
  }
@@ -1496,7 +1841,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1496
1841
  let lineToRender = line;
1497
1842
  const lineWidth = ctx.measureText(lineToRender).width;
1498
1843
  if (lineWidth > lineEffectiveMaxWidth) {
1499
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1844
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1845
+ lineToRender.length > 0) {
1500
1846
  lineToRender = lineToRender.slice(0, -1);
1501
1847
  }
1502
1848
  }
@@ -1517,7 +1863,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1517
1863
  let lineToRender = lineText;
1518
1864
  const lineWidth = ctx.measureText(lineToRender).width;
1519
1865
  if (lineWidth > shrunkEffectiveMaxWidth) {
1520
- while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth && lineToRender.length > 0) {
1866
+ while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
1867
+ lineToRender.length > 0) {
1521
1868
  lineToRender = lineToRender.slice(0, -1);
1522
1869
  }
1523
1870
  }
@@ -1580,7 +1927,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1580
1927
  const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
1581
1928
  testLines.length * lineHeight;
1582
1929
  // Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
1583
- const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
1930
+ const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
1584
1931
  const textEndX = x + testTextWidth;
1585
1932
  const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1586
1933
  const swatchesStartX = textEndX + spacing;
@@ -1609,7 +1956,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1609
1956
  else {
1610
1957
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1611
1958
  swatchX = swatchesStartX;
1612
- swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1959
+ swatchY =
1960
+ colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1613
1961
  // Render swatches (đã kiểm tra overflow ở trên)
1614
1962
  drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
1615
1963
  // cursorY đã được cập nhật ở trên
@@ -1625,10 +1973,7 @@ const prepareExportCanvas = async (config, options = {}) => {
1625
1973
  current: new Map(),
1626
1974
  };
1627
1975
  // Load fonts and images in parallel
1628
- await Promise.all([
1629
- preloadFonts(config),
1630
- preloadImages(config, imageRefs)
1631
- ]);
1976
+ await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
1632
1977
  renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
1633
1978
  if (!canvas.width || !canvas.height) {
1634
1979
  return null;