embroidery-qc-image 1.0.6 → 1.0.7
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 +545 -666
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +545 -666
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -31,73 +31,191 @@ 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
|
-
//
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// CONSTANTS
|
|
36
|
+
// ============================================================================
|
|
35
37
|
const COLOR_MAP = {
|
|
36
|
-
"Army (1394)": "#4B5320",
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
"Light Denim (1133)": "#B0C4DE",
|
|
67
|
-
"Light Denim": "#B0C4DE",
|
|
68
|
-
"Light Salmon (1018)": "#FFA07A",
|
|
69
|
-
"Light Salmon": "#FFA07A",
|
|
70
|
-
"Maroon (1374)": "#800000",
|
|
71
|
-
Maroon: "#800000",
|
|
72
|
-
"Navy Blue (1044)": "#000080",
|
|
73
|
-
"Navy Blue": "#000080",
|
|
74
|
-
"Olive Green (1157)": "#556B2F",
|
|
75
|
-
"Olive Green": "#556B2F",
|
|
76
|
-
"Orange (1278)": "#FFA500",
|
|
77
|
-
Orange: "#FFA500",
|
|
78
|
-
"Peach Blush (1053)": "#FFCCCB",
|
|
79
|
-
"Peach Blush": "#FFCCCB",
|
|
80
|
-
"Pink (1148)": "#FFC0CB",
|
|
81
|
-
Pink: "#FFC0CB",
|
|
82
|
-
"Purple (1412)": "#800080",
|
|
83
|
-
Purple: "#800080",
|
|
84
|
-
"Red (1037)": "#FF0000",
|
|
85
|
-
Red: "#FF0000",
|
|
86
|
-
"Silver Sage (1396)": "#A8A8A8",
|
|
87
|
-
"Silver Sage": "#A8A8A8",
|
|
88
|
-
"Summer Sky (1432)": "#87CEEB",
|
|
89
|
-
"Summer Sky": "#87CEEB",
|
|
90
|
-
"Terra Cotta (1477)": "#E2725B",
|
|
91
|
-
"Terra Cotta": "#E2725B",
|
|
92
|
-
"Sand (1055)": "#F4A460",
|
|
93
|
-
Sand: "#F4A460",
|
|
94
|
-
"White (9)": "#FFFFFF",
|
|
95
|
-
White: "#FFFFFF",
|
|
38
|
+
"Army (1394)": "#4B5320", Army: "#4B5320",
|
|
39
|
+
"Black (8)": "#000000", Black: "#000000",
|
|
40
|
+
"Bubblegum (1309)": "#FFC1CC", Bubblegum: "#FFC1CC",
|
|
41
|
+
"Carolina Blue (1274)": "#7BAFD4", "Carolina Blue": "#7BAFD4",
|
|
42
|
+
"Celadon (1098)": "#ACE1AF", Celadon: "#ACE1AF",
|
|
43
|
+
"Coffee Bean (1145)": "#6F4E37", "Coffee Bean": "#6F4E37",
|
|
44
|
+
"Daffodil (1180)": "#FFFF31", Daffodil: "#FFFF31",
|
|
45
|
+
"Dark Gray (1131)": "#A9A9A9", "Dark Gray": "#A9A9A9",
|
|
46
|
+
"Doe Skin Beige (1344)": "#F5E6D3", "Doe Skin Beige": "#F5E6D3",
|
|
47
|
+
"Dusty Blue (1373)": "#6699CC", "Dusty Blue": "#6699CC",
|
|
48
|
+
"Forest Green (1397)": "#228B22", "Forest Green": "#228B22",
|
|
49
|
+
"Gold (1425)": "#FFD700", Gold: "#FFD700",
|
|
50
|
+
"Gray (1118)": "#808080", Gray: "#808080",
|
|
51
|
+
"Ivory (1072)": "#FFFFF0", Ivory: "#FFFFF0",
|
|
52
|
+
"Lavender (1032)": "#E6E6FA", Lavender: "#E6E6FA",
|
|
53
|
+
"Light Denim (1133)": "#B0C4DE", "Light Denim": "#B0C4DE",
|
|
54
|
+
"Light Salmon (1018)": "#FFA07A", "Light Salmon": "#FFA07A",
|
|
55
|
+
"Maroon (1374)": "#800000", Maroon: "#800000",
|
|
56
|
+
"Navy Blue (1044)": "#000080", "Navy Blue": "#000080",
|
|
57
|
+
"Olive Green (1157)": "#556B2F", "Olive Green": "#556B2F",
|
|
58
|
+
"Orange (1278)": "#FFA500", Orange: "#FFA500",
|
|
59
|
+
"Peach Blush (1053)": "#FFCCCB", "Peach Blush": "#FFCCCB",
|
|
60
|
+
"Pink (1148)": "#FFC0CB", Pink: "#FFC0CB",
|
|
61
|
+
"Purple (1412)": "#800080", Purple: "#800080",
|
|
62
|
+
"Red (1037)": "#FF0000", Red: "#FF0000",
|
|
63
|
+
"Silver Sage (1396)": "#A8A8A8", "Silver Sage": "#A8A8A8",
|
|
64
|
+
"Summer Sky (1432)": "#87CEEB", "Summer Sky": "#87CEEB",
|
|
65
|
+
"Terra Cotta (1477)": "#E2725B", "Terra Cotta": "#E2725B",
|
|
66
|
+
"Sand (1055)": "#F4A460", Sand: "#F4A460",
|
|
67
|
+
"White (9)": "#FFFFFF", White: "#FFFFFF",
|
|
96
68
|
};
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
const BASE_URLS = {
|
|
70
|
+
FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
|
|
71
|
+
ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
|
|
72
|
+
FLORAL: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/florals",
|
|
73
|
+
THREAD_COLOR: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/thread-colors",
|
|
74
|
+
};
|
|
75
|
+
const LAYOUT = {
|
|
76
|
+
// Font families
|
|
77
|
+
HEADER_FONT_FAMILY: "Times New Roman",
|
|
78
|
+
FONT_FAMILY: "Arial",
|
|
79
|
+
// Font sizes (base values, will be multiplied by scaleFactor)
|
|
80
|
+
HEADER_FONT_SIZE: 220,
|
|
81
|
+
TEXT_FONT_SIZE: 200,
|
|
82
|
+
OTHER_FONT_SIZE: 160,
|
|
83
|
+
// Colors
|
|
84
|
+
HEADER_COLOR: "#000000",
|
|
85
|
+
LABEL_COLOR: "#444444",
|
|
86
|
+
BACKGROUND_COLOR: "#FFFFFF",
|
|
87
|
+
// Text alignment
|
|
88
|
+
TEXT_ALIGN: "left",
|
|
89
|
+
TEXT_BASELINE: "top",
|
|
90
|
+
// Spacing
|
|
91
|
+
LINE_GAP: 40,
|
|
92
|
+
PADDING: 40,
|
|
93
|
+
SECTION_SPACING: 60,
|
|
94
|
+
ELEMENT_SPACING: 100,
|
|
95
|
+
SWATCH_SPACING: 25,
|
|
96
|
+
FLORAL_SPACING: 300,
|
|
97
|
+
// Visual styling
|
|
98
|
+
SWATCH_HEIGHT_RATIO: 2.025,
|
|
99
|
+
UNDERLINE_POSITION: 0.9,
|
|
100
|
+
UNDERLINE_WIDTH: 10,
|
|
101
|
+
// Swatch reserved space
|
|
102
|
+
SWATCH_RESERVED_SPACE: 1000,
|
|
103
|
+
MIN_TEXT_WIDTH: 400,
|
|
104
|
+
};
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// HELPER FUNCTIONS
|
|
107
|
+
// ============================================================================
|
|
108
|
+
const loadFont = (fontName) => {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const fontUrl = `${BASE_URLS.FONT}/${encodeURIComponent(fontName)}.woff2`;
|
|
111
|
+
const fontFace = new FontFace(fontName, `url(${fontUrl})`);
|
|
112
|
+
fontFace
|
|
113
|
+
.load()
|
|
114
|
+
.then((loadedFont) => {
|
|
115
|
+
document.fonts.add(loadedFont);
|
|
116
|
+
resolve();
|
|
117
|
+
})
|
|
118
|
+
.catch(() => {
|
|
119
|
+
console.warn(`Could not load font ${fontName} from CDN`);
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const getImageUrl = (type, value) => {
|
|
125
|
+
if (type === "icon")
|
|
126
|
+
return `${BASE_URLS.ICON}/Icon ${value}.png`;
|
|
127
|
+
if (type === "floral")
|
|
128
|
+
return `${BASE_URLS.FLORAL}/${value}.png`;
|
|
129
|
+
return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
|
|
130
|
+
};
|
|
131
|
+
const loadImage = (url, imageRefs, onLoad) => {
|
|
132
|
+
if (imageRefs.current.has(url))
|
|
133
|
+
return;
|
|
134
|
+
const img = new Image();
|
|
135
|
+
img.crossOrigin = "anonymous";
|
|
136
|
+
img.src = url;
|
|
137
|
+
img.onload = onLoad;
|
|
138
|
+
imageRefs.current.set(url, img);
|
|
139
|
+
};
|
|
140
|
+
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
141
|
+
const words = text.split(" ");
|
|
142
|
+
const lines = [];
|
|
143
|
+
let currentLine = words[0];
|
|
144
|
+
for (let i = 1; i < words.length; i++) {
|
|
145
|
+
const testLine = currentLine + " " + words[i];
|
|
146
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
147
|
+
lines.push(currentLine);
|
|
148
|
+
currentLine = words[i];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
currentLine = testLine;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.push(currentLine);
|
|
155
|
+
let currentY = y;
|
|
156
|
+
lines.forEach((line) => {
|
|
157
|
+
ctx.fillText(line, x, currentY);
|
|
158
|
+
currentY += lineHeight;
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
height: lines.length * lineHeight,
|
|
162
|
+
lastLineWidth: ctx.measureText(lines[lines.length - 1]).width,
|
|
163
|
+
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
167
|
+
const words = text.split(" ");
|
|
168
|
+
const lines = [];
|
|
169
|
+
const lineStartIndices = [0];
|
|
170
|
+
let currentLine = words[0];
|
|
171
|
+
let currentCharIndex = words[0].length;
|
|
172
|
+
for (let i = 1; i < words.length; i++) {
|
|
173
|
+
const testLine = currentLine + " " + words[i];
|
|
174
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
175
|
+
lines.push(currentLine);
|
|
176
|
+
lineStartIndices.push(currentCharIndex + 1);
|
|
177
|
+
currentLine = words[i];
|
|
178
|
+
currentCharIndex += words[i].length + 1;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
currentLine = testLine;
|
|
182
|
+
currentCharIndex += words[i].length + 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.push(currentLine);
|
|
186
|
+
let currentY = y;
|
|
187
|
+
lines.forEach((line, lineIdx) => {
|
|
188
|
+
let currentX = x;
|
|
189
|
+
const startCharIdx = lineIdx > 0 ? lineStartIndices[lineIdx] : 0;
|
|
190
|
+
for (let i = 0; i < line.length; i++) {
|
|
191
|
+
const char = line[i];
|
|
192
|
+
const globalCharIdx = startCharIdx + i;
|
|
193
|
+
const colorIndex = globalCharIdx % colors.length;
|
|
194
|
+
const color = colors[colorIndex];
|
|
195
|
+
ctx.fillStyle = COLOR_MAP[color] || "#000000";
|
|
196
|
+
ctx.fillText(char, currentX, currentY);
|
|
197
|
+
currentX += ctx.measureText(char).width;
|
|
198
|
+
}
|
|
199
|
+
currentY += lineHeight;
|
|
200
|
+
});
|
|
201
|
+
return lines.length * lineHeight;
|
|
202
|
+
};
|
|
203
|
+
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
204
|
+
let swatchX = startX;
|
|
205
|
+
colors.forEach((color) => {
|
|
206
|
+
const url = getImageUrl("threadColor", color);
|
|
207
|
+
const img = imageRefs.current.get(url);
|
|
208
|
+
if (img && img.complete && img.naturalHeight > 0) {
|
|
209
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
210
|
+
const swatchW = Math.max(1, Math.floor(swatchHeight * ratio));
|
|
211
|
+
ctx.drawImage(img, swatchX, startY, swatchW, swatchHeight);
|
|
212
|
+
swatchX += swatchW + LAYOUT.SWATCH_SPACING * scaleFactor;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// MAIN COMPONENT
|
|
218
|
+
// ============================================================================
|
|
101
219
|
const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
102
220
|
const [canvasSize] = useState({ width: 4200, height: 4800 });
|
|
103
221
|
const [loadedFonts, setLoadedFonts] = useState(new Set());
|
|
@@ -107,7 +225,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
107
225
|
// Load fonts
|
|
108
226
|
useEffect(() => {
|
|
109
227
|
const loadFonts = async () => {
|
|
110
|
-
if (!config.sides
|
|
228
|
+
if (!config.sides?.length)
|
|
111
229
|
return;
|
|
112
230
|
const fontsToLoad = new Set();
|
|
113
231
|
config.sides.forEach((side) => {
|
|
@@ -118,14 +236,14 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
118
236
|
});
|
|
119
237
|
});
|
|
120
238
|
for (const fontName of fontsToLoad) {
|
|
121
|
-
if (loadedFonts.has(fontName))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
239
|
+
if (!loadedFonts.has(fontName)) {
|
|
240
|
+
try {
|
|
241
|
+
await loadFont(fontName);
|
|
242
|
+
setLoadedFonts((prev) => new Set(prev).add(fontName));
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.warn(`Could not load font ${fontName}:`, error);
|
|
246
|
+
}
|
|
129
247
|
}
|
|
130
248
|
}
|
|
131
249
|
};
|
|
@@ -133,649 +251,410 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
133
251
|
}, [config.sides, loadedFonts]);
|
|
134
252
|
// Load images
|
|
135
253
|
useEffect(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
setImagesLoaded((prev) => prev + 1);
|
|
149
|
-
};
|
|
150
|
-
img.onerror = () => {
|
|
151
|
-
if (useCors) {
|
|
152
|
-
// Retry without CORS; canvas may become tainted on export
|
|
153
|
-
loadMockup(false);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
img.src = config.image_url;
|
|
254
|
+
if (!config.sides?.length)
|
|
255
|
+
return;
|
|
256
|
+
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
257
|
+
// Load mockup
|
|
258
|
+
if (config.image_url) {
|
|
259
|
+
const loadMockup = (useCors) => {
|
|
260
|
+
const img = new Image();
|
|
261
|
+
if (useCors)
|
|
262
|
+
img.crossOrigin = "anonymous";
|
|
263
|
+
img.onload = () => {
|
|
264
|
+
imageRefs.current.set("mockup", img);
|
|
265
|
+
incrementCounter();
|
|
157
266
|
};
|
|
158
|
-
loadMockup(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
170
|
-
imageRefs.current.set(iconUrl, img);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
174
|
-
const floralUrl = `${FLORAL_BASE_URL}/${position.floral_pattern}.png`;
|
|
175
|
-
if (!imageRefs.current.has(floralUrl)) {
|
|
176
|
-
const img = new Image();
|
|
177
|
-
img.crossOrigin = "anonymous";
|
|
178
|
-
img.src = floralUrl;
|
|
179
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
180
|
-
imageRefs.current.set(floralUrl, img);
|
|
181
|
-
}
|
|
267
|
+
img.onerror = () => useCors && loadMockup(false);
|
|
268
|
+
img.src = config.image_url;
|
|
269
|
+
};
|
|
270
|
+
loadMockup(true);
|
|
271
|
+
}
|
|
272
|
+
// Load all other images
|
|
273
|
+
config.sides.forEach((side) => {
|
|
274
|
+
side.positions.forEach((position) => {
|
|
275
|
+
if (position.type === "ICON") {
|
|
276
|
+
if (position.icon !== 0) {
|
|
277
|
+
loadImage(getImageUrl("icon", position.icon), imageRefs, incrementCounter);
|
|
182
278
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
img.crossOrigin = "anonymous";
|
|
191
|
-
img.src = threadColorUrl;
|
|
192
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
193
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// Load character color images
|
|
197
|
-
if (position.character_colors &&
|
|
198
|
-
position.character_colors.length > 0) {
|
|
199
|
-
position.character_colors.forEach((color) => {
|
|
200
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
201
|
-
if (!imageRefs.current.has(threadColorUrl)) {
|
|
202
|
-
const img = new Image();
|
|
203
|
-
img.crossOrigin = "anonymous";
|
|
204
|
-
img.src = threadColorUrl;
|
|
205
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
206
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
279
|
+
position.layer_colors?.forEach((color) => {
|
|
280
|
+
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (position.type === "TEXT") {
|
|
284
|
+
if (position.floral_pattern) {
|
|
285
|
+
loadImage(getImageUrl("floral", position.floral_pattern), imageRefs, incrementCounter);
|
|
210
286
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
position.layer_colors.forEach((color) => {
|
|
214
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
215
|
-
if (!imageRefs.current.has(threadColorUrl)) {
|
|
216
|
-
const img = new Image();
|
|
217
|
-
img.crossOrigin = "anonymous";
|
|
218
|
-
img.src = threadColorUrl;
|
|
219
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
220
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
287
|
+
if (position.color) {
|
|
288
|
+
loadImage(getImageUrl("threadColor", position.color), imageRefs, incrementCounter);
|
|
223
289
|
}
|
|
224
|
-
|
|
290
|
+
position.character_colors?.forEach((color) => {
|
|
291
|
+
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
225
294
|
});
|
|
226
|
-
};
|
|
227
|
-
loadImages();
|
|
295
|
+
});
|
|
228
296
|
}, [config]);
|
|
229
297
|
// Render canvas
|
|
230
298
|
useEffect(() => {
|
|
231
299
|
const renderCanvas = () => {
|
|
232
|
-
if (!canvasRef.current || !config.sides
|
|
300
|
+
if (!canvasRef.current || !config.sides?.length)
|
|
233
301
|
return;
|
|
234
|
-
}
|
|
235
302
|
const canvas = canvasRef.current;
|
|
236
303
|
const ctx = canvas.getContext("2d");
|
|
237
304
|
if (!ctx)
|
|
238
305
|
return;
|
|
239
306
|
canvas.width = canvasSize.width;
|
|
240
307
|
canvas.height = canvasSize.height;
|
|
241
|
-
//
|
|
242
|
-
ctx.
|
|
308
|
+
// Set text alignment once
|
|
309
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
310
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
311
|
+
// Clear background
|
|
312
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
243
313
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
244
|
-
// Collect floral
|
|
314
|
+
// Collect floral assets
|
|
245
315
|
const floralAssets = [];
|
|
246
316
|
const seenFlorals = new Set();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
seenFlorals.add(floralUrl);
|
|
257
|
-
}
|
|
317
|
+
config.sides.forEach((side) => {
|
|
318
|
+
side.positions.forEach((position) => {
|
|
319
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
320
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
321
|
+
if (!seenFlorals.has(url)) {
|
|
322
|
+
const img = imageRefs.current.get(url);
|
|
323
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
324
|
+
floralAssets.push(img);
|
|
325
|
+
seenFlorals.add(url);
|
|
258
326
|
}
|
|
259
327
|
}
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
// Helper function to draw mockup and florals
|
|
264
|
-
const drawMockupAndFlorals = () => {
|
|
265
|
-
const mockupImg = imageRefs.current.get("mockup");
|
|
266
|
-
const margin = 40; // small padding
|
|
267
|
-
let mockupBox = null;
|
|
268
|
-
if (mockupImg && mockupImg.complete && mockupImg.naturalWidth > 0) {
|
|
269
|
-
const maxTargetWidth = Math.min(1800, canvas.width * 0.375);
|
|
270
|
-
const maxTargetHeight = canvas.height * 0.375;
|
|
271
|
-
const scale = Math.min(maxTargetWidth / mockupImg.naturalWidth, maxTargetHeight / mockupImg.naturalHeight);
|
|
272
|
-
const targetWidth = Math.max(1, Math.floor(mockupImg.naturalWidth * scale));
|
|
273
|
-
const targetHeight = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
274
|
-
const targetX = canvas.width - margin - targetWidth;
|
|
275
|
-
const targetY = canvas.height - margin - targetHeight;
|
|
276
|
-
mockupBox = {
|
|
277
|
-
x: targetX,
|
|
278
|
-
y: targetY,
|
|
279
|
-
w: targetWidth,
|
|
280
|
-
h: targetHeight,
|
|
281
|
-
};
|
|
282
|
-
ctx.drawImage(mockupImg, targetX, targetY, targetWidth, targetHeight);
|
|
283
|
-
}
|
|
284
|
-
// Draw florals to the left of mockup
|
|
285
|
-
if (mockupBox && floralAssets.length > 0) {
|
|
286
|
-
const spacing = 300;
|
|
287
|
-
const targetHeight = mockupBox.h;
|
|
288
|
-
const floralFixedH = Math.min(900, targetHeight);
|
|
289
|
-
let currentX = mockupBox.x - spacing;
|
|
290
|
-
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
291
|
-
const img = floralAssets[i];
|
|
292
|
-
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
|
|
293
|
-
const h = floralFixedH;
|
|
294
|
-
const w = Math.max(1, Math.floor(h * ratio));
|
|
295
|
-
currentX -= w;
|
|
296
|
-
const y = mockupBox.y + (targetHeight - h);
|
|
297
|
-
ctx.drawImage(img, currentX, y, w, h);
|
|
298
|
-
currentX -= spacing;
|
|
299
328
|
}
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
//
|
|
303
|
-
// This allows text to overlay images when needed
|
|
304
|
-
// Pass 1: Measure actual height with original size (use offscreen canvas for measurement)
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// Calculate scale factor
|
|
305
332
|
const measureCanvas = document.createElement("canvas");
|
|
306
333
|
measureCanvas.width = canvas.width;
|
|
307
334
|
measureCanvas.height = canvas.height;
|
|
308
335
|
const measureCtx = measureCanvas.getContext("2d");
|
|
309
336
|
if (!measureCtx)
|
|
310
337
|
return;
|
|
311
|
-
|
|
312
|
-
measureCtx.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
let measureY = 40;
|
|
316
|
-
const measureSpacing = 100;
|
|
338
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
339
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
340
|
+
let measureY = LAYOUT.PADDING;
|
|
341
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
317
342
|
config.sides.forEach((side) => {
|
|
318
|
-
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1);
|
|
343
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
319
344
|
measureY += sideHeight + measureSpacing;
|
|
320
345
|
});
|
|
321
|
-
const
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const targetContentHeight = canvas.height - topPadding;
|
|
327
|
-
// Only scale down if content exceeds canvas height
|
|
328
|
-
// Never scale up - preserve original font sizes
|
|
329
|
-
let scaleFactor = 1;
|
|
330
|
-
if (totalMeasuredHeight > targetContentHeight) {
|
|
331
|
-
// Scale down to fit exactly
|
|
332
|
-
scaleFactor = targetContentHeight / totalMeasuredHeight;
|
|
333
|
-
scaleFactor = Math.max(0.5, scaleFactor); // Minimum scale to prevent tiny fonts
|
|
334
|
-
}
|
|
335
|
-
// If content fits, keep scaleFactor = 1 (original font sizes)
|
|
336
|
-
// Draw mockup and florals first (bottom layer)
|
|
337
|
-
drawMockupAndFlorals();
|
|
338
|
-
// Draw content on top (top layer) - text will overlay images if needed
|
|
339
|
-
let currentY = topPadding * scaleFactor;
|
|
346
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
347
|
+
// Draw mockup and florals
|
|
348
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
349
|
+
// Draw content
|
|
350
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
340
351
|
config.sides.forEach((side) => {
|
|
341
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor);
|
|
352
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
342
353
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
343
354
|
});
|
|
344
355
|
};
|
|
345
|
-
// Delay rendering to ensure fonts and images are loaded
|
|
346
356
|
const timer = setTimeout(renderCanvas, 100);
|
|
347
357
|
return () => clearTimeout(timer);
|
|
348
358
|
}, [config, canvasSize, loadedFonts, imagesLoaded]);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
height
|
|
377
|
-
|
|
378
|
-
lastLineY,
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
|
-
// Helper to wrap and draw multi-color text (for character_colors)
|
|
382
|
-
const fillTextWrappedMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
383
|
-
const words = text.split(" ");
|
|
384
|
-
const lines = [];
|
|
385
|
-
const lineStartIndices = [0];
|
|
386
|
-
let currentLine = words[0];
|
|
387
|
-
let currentCharIndex = words[0].length;
|
|
388
|
-
for (let i = 1; i < words.length; i++) {
|
|
389
|
-
const word = words[i];
|
|
390
|
-
const testLine = currentLine + " " + word;
|
|
391
|
-
const metrics = ctx.measureText(testLine);
|
|
392
|
-
if (metrics.width > maxWidth && currentLine.length > 0) {
|
|
393
|
-
lines.push(currentLine);
|
|
394
|
-
lineStartIndices.push(currentCharIndex + 1); // +1 for space
|
|
395
|
-
currentLine = word;
|
|
396
|
-
currentCharIndex += word.length + 1;
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
currentLine = testLine;
|
|
400
|
-
currentCharIndex += word.length + 1;
|
|
401
|
-
}
|
|
359
|
+
return (jsx("div", { className: `render-embroidery${className ? ` ${className}` : ""}`, style: style, children: jsx("canvas", { ref: canvasRef, className: "render-embroidery-canvas" }) }));
|
|
360
|
+
};
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// RENDERING FUNCTIONS
|
|
363
|
+
// ============================================================================
|
|
364
|
+
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
365
|
+
const mockupImg = imageRefs.current.get("mockup");
|
|
366
|
+
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
367
|
+
return;
|
|
368
|
+
const margin = LAYOUT.PADDING;
|
|
369
|
+
const maxWidth = Math.min(1800, canvas.width * 0.375);
|
|
370
|
+
const maxHeight = canvas.height * 0.375;
|
|
371
|
+
const scale = Math.min(maxWidth / mockupImg.naturalWidth, maxHeight / mockupImg.naturalHeight);
|
|
372
|
+
const width = Math.max(1, Math.floor(mockupImg.naturalWidth * scale));
|
|
373
|
+
const height = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
374
|
+
const x = canvas.width - margin - width;
|
|
375
|
+
const y = canvas.height - margin - height;
|
|
376
|
+
ctx.drawImage(mockupImg, x, y, width, height);
|
|
377
|
+
// Draw florals
|
|
378
|
+
if (floralAssets.length > 0) {
|
|
379
|
+
const floralH = Math.min(900, height);
|
|
380
|
+
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
381
|
+
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
382
|
+
const img = floralAssets[i];
|
|
383
|
+
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
|
|
384
|
+
const w = Math.max(1, Math.floor(floralH * ratio));
|
|
385
|
+
currentX -= w;
|
|
386
|
+
ctx.drawImage(img, currentX, y + height - floralH, w, floralH);
|
|
387
|
+
currentX -= LAYOUT.FLORAL_SPACING;
|
|
402
388
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
392
|
+
let currentY = startY;
|
|
393
|
+
const padding = LAYOUT.PADDING * scaleFactor;
|
|
394
|
+
const sideWidth = width - 2 * padding;
|
|
395
|
+
// Draw header
|
|
396
|
+
ctx.save();
|
|
397
|
+
const headerFontSize = LAYOUT.HEADER_FONT_SIZE * scaleFactor;
|
|
398
|
+
ctx.font = `bold ${headerFontSize}px ${LAYOUT.HEADER_FONT_FAMILY}`;
|
|
399
|
+
ctx.fillStyle = LAYOUT.HEADER_COLOR;
|
|
400
|
+
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
|
|
401
|
+
// Draw underline
|
|
402
|
+
const underlineY = headerResult.lastLineY + headerFontSize * LAYOUT.UNDERLINE_POSITION;
|
|
403
|
+
ctx.strokeStyle = LAYOUT.HEADER_COLOR;
|
|
404
|
+
ctx.lineWidth = LAYOUT.UNDERLINE_WIDTH * scaleFactor;
|
|
405
|
+
ctx.beginPath();
|
|
406
|
+
ctx.moveTo(padding, underlineY);
|
|
407
|
+
ctx.lineTo(padding + headerResult.lastLineWidth, underlineY);
|
|
408
|
+
ctx.stroke();
|
|
409
|
+
currentY += headerResult.height + LAYOUT.SECTION_SPACING * scaleFactor;
|
|
410
|
+
ctx.restore();
|
|
411
|
+
// Compute uniform properties
|
|
412
|
+
const textPositions = side.positions.filter((p) => p.type === "TEXT");
|
|
413
|
+
const uniformProps = computeUniformProperties(textPositions);
|
|
414
|
+
// Render uniform labels (only if more than 1 TEXT position)
|
|
415
|
+
if (textPositions.length > 1) {
|
|
416
|
+
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
417
|
+
}
|
|
418
|
+
// Group text positions by common properties
|
|
419
|
+
const textGroups = groupTextPositions(textPositions);
|
|
420
|
+
// Render text positions (with proper spacing between groups)
|
|
421
|
+
let textCounter = 1;
|
|
422
|
+
textGroups.forEach((group, groupIndex) => {
|
|
423
|
+
group.forEach((position, index) => {
|
|
424
|
+
// Add extra spacing between different groups
|
|
425
|
+
if (index === 0 && groupIndex !== 0) {
|
|
426
|
+
currentY += LAYOUT.SECTION_SPACING * scaleFactor;
|
|
416
427
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
let currentProps = null;
|
|
431
|
-
side.positions.forEach((position) => {
|
|
432
|
-
if (position.type === "TEXT") {
|
|
433
|
-
if (!currentGroup ||
|
|
434
|
-
currentProps.font !== position.font ||
|
|
435
|
-
currentProps.text_shape !== position.text_shape ||
|
|
436
|
-
currentProps.color !== position.color ||
|
|
437
|
-
currentProps.character_colors?.join(",") !==
|
|
438
|
-
position.character_colors?.join(",")) {
|
|
439
|
-
// Start new group
|
|
440
|
-
if (currentGroup) {
|
|
441
|
-
textGroups.push({
|
|
442
|
-
positions: currentGroup,
|
|
443
|
-
properties: currentProps,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
currentGroup = [position];
|
|
447
|
-
currentProps = {
|
|
448
|
-
font: position.font,
|
|
449
|
-
text_shape: position.text_shape,
|
|
450
|
-
color: position.color,
|
|
451
|
-
character_colors: position.character_colors,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
currentGroup.push(position);
|
|
456
|
-
}
|
|
428
|
+
// If only 1 TEXT position, show all labels (no uniform labels rendered)
|
|
429
|
+
const showLabels = textPositions.length === 1
|
|
430
|
+
? { font: true, shape: true, floral: true, color: true }
|
|
431
|
+
: {
|
|
432
|
+
font: !uniformProps.isUniform.font,
|
|
433
|
+
shape: !uniformProps.isUniform.shape,
|
|
434
|
+
floral: !uniformProps.isUniform.floral,
|
|
435
|
+
color: !uniformProps.isUniform.color,
|
|
436
|
+
};
|
|
437
|
+
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs);
|
|
438
|
+
if (height > 0) {
|
|
439
|
+
currentY += height + LAYOUT.PADDING * scaleFactor;
|
|
440
|
+
textCounter++;
|
|
457
441
|
}
|
|
458
442
|
});
|
|
459
|
-
|
|
460
|
-
|
|
443
|
+
});
|
|
444
|
+
// Render icon positions
|
|
445
|
+
currentY += LAYOUT.LINE_GAP * scaleFactor;
|
|
446
|
+
side.positions.forEach((position) => {
|
|
447
|
+
if (position.type === "ICON") {
|
|
448
|
+
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
449
|
+
currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
|
|
461
450
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (position.type === "TEXT")
|
|
476
|
-
allTextPositions.push(position);
|
|
477
|
-
});
|
|
478
|
-
const sideFonts = new Set(allTextPositions.map((p) => p.font));
|
|
479
|
-
const sideShapes = new Set(allTextPositions.map((p) => p.text_shape));
|
|
480
|
-
const sideFlorals = new Set(allTextPositions.map((p) => p.floral_pattern ?? "None"));
|
|
481
|
-
const colorKeyOf = (p) => p.character_colors && p.character_colors.length > 0
|
|
482
|
-
? p.character_colors.join(",")
|
|
483
|
-
: p.color ?? "None";
|
|
484
|
-
const sideColors = new Set(allTextPositions.map((p) => colorKeyOf(p)));
|
|
485
|
-
const sideUniform = {
|
|
486
|
-
font: sideFonts.size === 1,
|
|
487
|
-
shape: sideShapes.size === 1,
|
|
488
|
-
floral: sideFlorals.size === 1,
|
|
489
|
-
color: sideColors.size === 1,
|
|
451
|
+
});
|
|
452
|
+
return currentY - startY;
|
|
453
|
+
};
|
|
454
|
+
const groupTextPositions = (textPositions) => {
|
|
455
|
+
const groups = [];
|
|
456
|
+
let currentGroup = null;
|
|
457
|
+
let currentProps = null;
|
|
458
|
+
textPositions.forEach((position) => {
|
|
459
|
+
const posProps = {
|
|
460
|
+
font: position.font,
|
|
461
|
+
text_shape: position.text_shape,
|
|
462
|
+
color: position.color,
|
|
463
|
+
character_colors: position.character_colors?.join(","),
|
|
490
464
|
};
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// Render text groups first
|
|
499
|
-
let sideTextCounter = 1;
|
|
500
|
-
textGroups.forEach((group, groupIndex) => {
|
|
501
|
-
group.positions.forEach((position, index) => {
|
|
502
|
-
if (index === 0 && groupIndex !== 0)
|
|
503
|
-
currentY += 50 * scaleFactor;
|
|
504
|
-
const drawnHeight = renderText(ctx, position, padding, currentY, sideWidth, sideTextCounter, {
|
|
505
|
-
font: !sideUniform.font,
|
|
506
|
-
shape: !sideUniform.shape,
|
|
507
|
-
floral: !sideUniform.floral,
|
|
508
|
-
color: !sideUniform.color,
|
|
509
|
-
}, scaleFactor);
|
|
510
|
-
sideTextCounter += 1;
|
|
511
|
-
// add padding only if something was actually drawn
|
|
512
|
-
if (drawnHeight > 0) {
|
|
513
|
-
currentY += drawnHeight + 40 * scaleFactor;
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
// Render ICON titles/values (no images here)
|
|
518
|
-
currentY += 30 * scaleFactor; // minimal spacing before icon labels
|
|
519
|
-
side.positions.forEach((position) => {
|
|
520
|
-
if (position.type === "ICON") {
|
|
521
|
-
currentY += renderIconLabels(ctx, position, padding, currentY, sideWidth, scaleFactor);
|
|
522
|
-
currentY += 10 * scaleFactor;
|
|
465
|
+
if (!currentGroup ||
|
|
466
|
+
currentProps.font !== posProps.font ||
|
|
467
|
+
currentProps.text_shape !== posProps.text_shape ||
|
|
468
|
+
currentProps.color !== posProps.color ||
|
|
469
|
+
currentProps.character_colors !== posProps.character_colors) {
|
|
470
|
+
if (currentGroup) {
|
|
471
|
+
groups.push(currentGroup);
|
|
523
472
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
};
|
|
527
|
-
const renderSideUniformLabels = (ctx, values, x, y, maxWidth, scaleFactor = 1) => {
|
|
528
|
-
const labelFontFamily = "Arial";
|
|
529
|
-
const fontSize = 180 * scaleFactor;
|
|
530
|
-
const lineGap = 20 * scaleFactor;
|
|
531
|
-
ctx.save();
|
|
532
|
-
ctx.font = `${fontSize}px ${labelFontFamily}`;
|
|
533
|
-
ctx.textAlign = "left";
|
|
534
|
-
ctx.textBaseline = "top";
|
|
535
|
-
ctx.fillStyle = "#444444";
|
|
536
|
-
let cursorY = y;
|
|
537
|
-
let rendered = 0;
|
|
538
|
-
if (values.font) {
|
|
539
|
-
const fontText = `Font: ${values.font}`;
|
|
540
|
-
const result = fillTextWrapped(ctx, fontText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
541
|
-
cursorY += result.height;
|
|
542
|
-
rendered++;
|
|
543
|
-
}
|
|
544
|
-
if (values.shape && values.shape !== "None") {
|
|
545
|
-
const shapeText = `Kiểu chữ: ${values.shape}`;
|
|
546
|
-
const result = fillTextWrapped(ctx, shapeText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
547
|
-
cursorY += result.height;
|
|
548
|
-
rendered++;
|
|
549
|
-
}
|
|
550
|
-
if (values.color && values.color !== "None") {
|
|
551
|
-
const colorText = `Màu chỉ: ${values.color}`;
|
|
552
|
-
// Reserve space for swatches (estimate: max 5 swatches × 200px each = 1000px)
|
|
553
|
-
const swatchReserved = 1000 * scaleFactor;
|
|
554
|
-
const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
|
|
555
|
-
const result = fillTextWrapped(ctx, colorText, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
556
|
-
// Draw swatches inline for side-level color, preserving aspect ratio; 75% of previous size
|
|
557
|
-
// Position swatches after the last line of wrapped text
|
|
558
|
-
const swatchH = Math.floor(fontSize * 2.025);
|
|
559
|
-
let swatchX = x + Math.ceil(result.lastLineWidth) + 100 * scaleFactor;
|
|
560
|
-
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
561
|
-
const colorTokens = values.color.includes(",")
|
|
562
|
-
? values.color.split(",").map((s) => s.trim())
|
|
563
|
-
: [values.color];
|
|
564
|
-
colorTokens.forEach((color) => {
|
|
565
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
566
|
-
const img = imageRefs.current.get(threadColorUrl);
|
|
567
|
-
if (img && img.complete && img.naturalHeight > 0) {
|
|
568
|
-
const ratio = img.naturalWidth / img.naturalHeight;
|
|
569
|
-
const swatchW = Math.max(1, Math.floor(swatchH * ratio));
|
|
570
|
-
ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
|
|
571
|
-
swatchX += swatchW + 25 * scaleFactor;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
cursorY += result.height;
|
|
575
|
-
rendered++;
|
|
576
|
-
}
|
|
577
|
-
if (values.floral && values.floral !== "None") {
|
|
578
|
-
const floralText = `Mẫu hoa: ${values.floral}`;
|
|
579
|
-
const result = fillTextWrapped(ctx, floralText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
580
|
-
cursorY += result.height;
|
|
581
|
-
rendered++;
|
|
582
|
-
}
|
|
583
|
-
if (rendered > 0)
|
|
584
|
-
cursorY += 50 * scaleFactor; // extra gap before first text line
|
|
585
|
-
ctx.restore();
|
|
586
|
-
return cursorY - y;
|
|
587
|
-
};
|
|
588
|
-
const renderText = (ctx, position, x, y, maxWidth, displayIndex, showLabels, scaleFactor = 1) => {
|
|
589
|
-
ctx.save();
|
|
590
|
-
// Info labels
|
|
591
|
-
// Unified font sizing for labels and content (side title uses its own larger size)
|
|
592
|
-
const infoLineGap = 30 * scaleFactor;
|
|
593
|
-
const labelFontFamily = "Arial";
|
|
594
|
-
// Use a unified content font size for both labels and text content
|
|
595
|
-
const fontSize = 180 * scaleFactor;
|
|
596
|
-
const infoFontSize = fontSize;
|
|
597
|
-
ctx.font = `${infoFontSize}px ${labelFontFamily}`;
|
|
598
|
-
ctx.textAlign = "left";
|
|
599
|
-
ctx.textBaseline = "top";
|
|
600
|
-
ctx.fillStyle = "#444444";
|
|
601
|
-
let currentYCursor = y;
|
|
602
|
-
let drawnHeight = 0; // accumulate only when something is actually drawn
|
|
603
|
-
// Text value with unified font size
|
|
604
|
-
const displayText = position.text;
|
|
605
|
-
ctx.textAlign = "left";
|
|
606
|
-
ctx.textBaseline = "top";
|
|
607
|
-
// Label for text line
|
|
608
|
-
const textLabel = `Text ${displayIndex}: `;
|
|
609
|
-
ctx.font = `bold ${fontSize}px ${labelFontFamily}`;
|
|
610
|
-
const labelWidth = ctx.measureText(textLabel).width;
|
|
611
|
-
ctx.fillStyle = "#444444";
|
|
612
|
-
ctx.fillText(textLabel, x, currentYCursor);
|
|
613
|
-
// Calculate available width for text content
|
|
614
|
-
const textMaxWidth = maxWidth - labelWidth;
|
|
615
|
-
// Handle character_colors (alternating colors)
|
|
616
|
-
if (position.character_colors && position.character_colors.length > 0) {
|
|
617
|
-
// Switch to content font
|
|
618
|
-
ctx.font = `${fontSize}px ${position.font}`;
|
|
619
|
-
const textHeight = fillTextWrappedMultiColor(ctx, displayText, position.character_colors, x + labelWidth, currentYCursor, textMaxWidth, fontSize);
|
|
620
|
-
currentYCursor += textHeight;
|
|
621
|
-
drawnHeight += textHeight;
|
|
473
|
+
currentGroup = [position];
|
|
474
|
+
currentProps = posProps;
|
|
622
475
|
}
|
|
623
476
|
else {
|
|
624
|
-
|
|
625
|
-
// Draw text in content font, black (non-bold)
|
|
626
|
-
ctx.font = `${fontSize}px ${position.font}`;
|
|
627
|
-
ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || "#000000";
|
|
628
|
-
const textResult = fillTextWrapped(ctx, displayText, x + labelWidth, currentYCursor, textMaxWidth, fontSize);
|
|
629
|
-
currentYCursor += textResult.height;
|
|
630
|
-
drawnHeight += textResult.height;
|
|
631
|
-
}
|
|
632
|
-
// After text, print Kiểu chữ (when not uniform), then Font and Color as needed
|
|
633
|
-
currentYCursor += infoLineGap;
|
|
634
|
-
ctx.font = `${infoFontSize}px ${labelFontFamily}`;
|
|
635
|
-
ctx.fillStyle = "#444444";
|
|
636
|
-
if (showLabels.shape && position.text_shape) {
|
|
637
|
-
const shapeLabelAfter = `Kiểu chữ: ${position.text_shape}`;
|
|
638
|
-
const result = fillTextWrapped(ctx, shapeLabelAfter, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
|
|
639
|
-
currentYCursor += result.height;
|
|
640
|
-
drawnHeight += result.height;
|
|
641
|
-
}
|
|
642
|
-
if (showLabels.font && position.font) {
|
|
643
|
-
const fontLabel = `Font: ${position.font}`;
|
|
644
|
-
const result = fillTextWrapped(ctx, fontLabel, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
|
|
645
|
-
currentYCursor += result.height;
|
|
646
|
-
drawnHeight += result.height;
|
|
647
|
-
}
|
|
648
|
-
if (showLabels.color) {
|
|
649
|
-
let colorLabelValue = "None";
|
|
650
|
-
if (position.character_colors && position.character_colors.length > 0) {
|
|
651
|
-
colorLabelValue = position.character_colors.join(", ");
|
|
652
|
-
}
|
|
653
|
-
else if (position.color) {
|
|
654
|
-
colorLabelValue = position.color;
|
|
655
|
-
}
|
|
656
|
-
if (colorLabelValue !== "None") {
|
|
657
|
-
const colorLabel = `Màu chỉ: ${colorLabelValue}`;
|
|
658
|
-
// Reserve space for swatches
|
|
659
|
-
const swatchReserved = 1000 * scaleFactor;
|
|
660
|
-
const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
|
|
661
|
-
const result = fillTextWrapped(ctx, colorLabel, x, currentYCursor, textMaxWidth, infoFontSize + infoLineGap);
|
|
662
|
-
// Draw color swatch images inline with Color label for TEXT, preserve aspect ratio; 75% of previous size
|
|
663
|
-
// Position swatches after the last line of wrapped text
|
|
664
|
-
const swatchH = Math.floor(infoFontSize * 2.025);
|
|
665
|
-
let swatchX = x + Math.ceil(result.lastLineWidth) + 100 * scaleFactor; // spacing after text
|
|
666
|
-
const swatchY = result.lastLineY + Math.floor(infoFontSize / 2 - swatchH / 2);
|
|
667
|
-
if (position.character_colors && position.character_colors.length > 0) {
|
|
668
|
-
position.character_colors.forEach((color) => {
|
|
669
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
670
|
-
const img = imageRefs.current.get(threadColorUrl);
|
|
671
|
-
if (img && img.complete && img.naturalHeight > 0) {
|
|
672
|
-
const ratio = img.naturalWidth / img.naturalHeight;
|
|
673
|
-
const swatchW = Math.max(1, Math.floor(swatchH * ratio));
|
|
674
|
-
ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
|
|
675
|
-
swatchX += swatchW + 25 * scaleFactor;
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
else if (position.color) {
|
|
680
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${position.color}.png`;
|
|
681
|
-
const img = imageRefs.current.get(threadColorUrl);
|
|
682
|
-
if (img && img.complete && img.naturalHeight > 0) {
|
|
683
|
-
const ratio = img.naturalWidth / img.naturalHeight;
|
|
684
|
-
const swatchW = Math.max(1, Math.floor(swatchH * ratio));
|
|
685
|
-
ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
currentYCursor += result.height;
|
|
689
|
-
drawnHeight += result.height;
|
|
690
|
-
}
|
|
477
|
+
currentGroup.push(position);
|
|
691
478
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
479
|
+
});
|
|
480
|
+
if (currentGroup) {
|
|
481
|
+
groups.push(currentGroup);
|
|
482
|
+
}
|
|
483
|
+
return groups;
|
|
484
|
+
};
|
|
485
|
+
const computeUniformProperties = (textPositions) => {
|
|
486
|
+
if (textPositions.length === 0) {
|
|
487
|
+
return {
|
|
488
|
+
values: { font: null, shape: null, floral: null, color: null },
|
|
489
|
+
isUniform: { font: false, shape: false, floral: false, color: false },
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const fonts = new Set(textPositions.map((p) => p.font));
|
|
493
|
+
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
494
|
+
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
495
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length ? p.character_colors.join(",") : p.color ?? "None"));
|
|
496
|
+
return {
|
|
497
|
+
values: {
|
|
498
|
+
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
499
|
+
shape: shapes.size === 1 ? [...shapes][0] : null,
|
|
500
|
+
floral: florals.size === 1 ? [...florals][0] : null,
|
|
501
|
+
color: colors.size === 1 ? [...colors][0] : null,
|
|
502
|
+
},
|
|
503
|
+
isUniform: {
|
|
504
|
+
font: fonts.size === 1,
|
|
505
|
+
shape: shapes.size === 1,
|
|
506
|
+
floral: florals.size === 1,
|
|
507
|
+
color: colors.size === 1,
|
|
508
|
+
},
|
|
702
509
|
};
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
510
|
+
};
|
|
511
|
+
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs) => {
|
|
512
|
+
const { values } = uniformProps;
|
|
513
|
+
const fontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
514
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
515
|
+
ctx.save();
|
|
516
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
517
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
518
|
+
let cursorY = y;
|
|
519
|
+
let rendered = 0;
|
|
520
|
+
if (values.font) {
|
|
521
|
+
const result = wrapText(ctx, `Font: ${values.font}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
522
|
+
cursorY += result.height;
|
|
523
|
+
rendered++;
|
|
524
|
+
}
|
|
525
|
+
if (values.shape && values.shape !== "None") {
|
|
526
|
+
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
527
|
+
cursorY += result.height;
|
|
528
|
+
rendered++;
|
|
529
|
+
}
|
|
530
|
+
if (values.color && values.color !== "None") {
|
|
531
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
532
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
533
|
+
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
534
|
+
const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
535
|
+
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
536
|
+
const colors = values.color.includes(",") ? values.color.split(",").map((s) => s.trim()) : [values.color];
|
|
537
|
+
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
538
|
+
cursorY += result.height;
|
|
539
|
+
rendered++;
|
|
540
|
+
}
|
|
541
|
+
if (values.floral && values.floral !== "None") {
|
|
542
|
+
const result = wrapText(ctx, `Mẫu hoa: ${values.floral}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
543
|
+
cursorY += result.height;
|
|
544
|
+
rendered++;
|
|
545
|
+
}
|
|
546
|
+
if (rendered > 0)
|
|
547
|
+
cursorY += LAYOUT.SECTION_SPACING * scaleFactor;
|
|
548
|
+
ctx.restore();
|
|
549
|
+
return cursorY - y;
|
|
550
|
+
};
|
|
551
|
+
const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
552
|
+
ctx.save();
|
|
553
|
+
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
554
|
+
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
555
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
556
|
+
let currentY = y;
|
|
557
|
+
let drawnHeight = 0;
|
|
558
|
+
// Draw label
|
|
559
|
+
const textLabel = `Text ${displayIndex}: `;
|
|
560
|
+
ctx.font = `bold ${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
561
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
562
|
+
const labelWidth = ctx.measureText(textLabel).width;
|
|
563
|
+
ctx.fillText(textLabel, x, currentY);
|
|
564
|
+
const textMaxWidth = maxWidth - labelWidth;
|
|
565
|
+
// Get display text (handle empty/null/undefined)
|
|
566
|
+
const isEmptyText = !position.text || position.text.trim() === "";
|
|
567
|
+
// Draw text content
|
|
568
|
+
if (isEmptyText) {
|
|
569
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
570
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
571
|
+
const textResult = wrapText(ctx, "không thay đổi", x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
572
|
+
currentY += textResult.height;
|
|
573
|
+
drawnHeight += textResult.height;
|
|
574
|
+
}
|
|
575
|
+
else if (position.character_colors?.length) {
|
|
576
|
+
ctx.font = `${textFontSize}px ${position.font}`;
|
|
577
|
+
const textHeight = wrapTextMultiColor(ctx, position.text, position.character_colors, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
578
|
+
currentY += textHeight;
|
|
579
|
+
drawnHeight += textHeight;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
ctx.font = `${textFontSize}px ${position.font}`;
|
|
583
|
+
ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || "#000000";
|
|
584
|
+
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
585
|
+
currentY += textResult.height;
|
|
586
|
+
drawnHeight += textResult.height;
|
|
587
|
+
}
|
|
588
|
+
// Draw additional labels
|
|
589
|
+
currentY += lineGap;
|
|
590
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
591
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
592
|
+
if (showLabels.shape && position.text_shape) {
|
|
593
|
+
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
594
|
+
currentY += result.height;
|
|
595
|
+
drawnHeight += result.height;
|
|
596
|
+
}
|
|
597
|
+
if (showLabels.font && position.font) {
|
|
598
|
+
const result = wrapText(ctx, `Font: ${position.font}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
599
|
+
currentY += result.height;
|
|
600
|
+
drawnHeight += result.height;
|
|
601
|
+
}
|
|
602
|
+
if (showLabels.color) {
|
|
603
|
+
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
604
|
+
if (colorValue) {
|
|
605
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
606
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
607
|
+
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
608
|
+
const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
609
|
+
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
610
|
+
const colors = position.character_colors || [position.color];
|
|
611
|
+
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
612
|
+
currentY += result.height;
|
|
613
|
+
drawnHeight += result.height;
|
|
730
614
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
615
|
+
}
|
|
616
|
+
if (showLabels.floral && position.floral_pattern) {
|
|
617
|
+
const result = wrapText(ctx, `Mẫu hoa: ${position.floral_pattern}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
618
|
+
currentY += result.height;
|
|
619
|
+
drawnHeight += result.height;
|
|
620
|
+
}
|
|
621
|
+
ctx.restore();
|
|
622
|
+
return drawnHeight;
|
|
623
|
+
};
|
|
624
|
+
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs) => {
|
|
625
|
+
const iconFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
626
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
627
|
+
ctx.save();
|
|
628
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
629
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
630
|
+
let cursorY = y;
|
|
631
|
+
const iconText = position.icon === 0 ? `Icon: icon mặc định theo file thêu` : `Icon: ${position.icon}`;
|
|
632
|
+
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
633
|
+
// Draw icon image
|
|
634
|
+
if (position.icon !== 0) {
|
|
635
|
+
const url = getImageUrl("icon", position.icon);
|
|
636
|
+
const img = imageRefs.current.get(url);
|
|
637
|
+
if (img?.complete && img.naturalHeight > 0) {
|
|
638
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
639
|
+
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
640
|
+
const iconX = x + Math.ceil(iconResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
641
|
+
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
642
|
+
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
756
643
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.catch(() => {
|
|
772
|
-
// Font loading failed, will use fallback
|
|
773
|
-
console.warn(`Could not load font ${fontName} from CDN`);
|
|
774
|
-
resolve(); // Still resolve to not block rendering
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
};
|
|
778
|
-
return (jsx("div", { className: `render-embroidery${className ? ` ${className}` : ""}`, style: style, children: jsx("canvas", { ref: canvasRef, className: "render-embroidery-canvas" }) }));
|
|
644
|
+
}
|
|
645
|
+
cursorY += iconResult.height;
|
|
646
|
+
// Draw color swatches
|
|
647
|
+
if (position.layer_colors?.length) {
|
|
648
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
649
|
+
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
650
|
+
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
651
|
+
const swatchX = x + Math.ceil(colorResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
652
|
+
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
653
|
+
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
654
|
+
cursorY += colorResult.height;
|
|
655
|
+
}
|
|
656
|
+
ctx.restore();
|
|
657
|
+
return cursorY - y;
|
|
779
658
|
};
|
|
780
659
|
|
|
781
660
|
export { EmbroideryQCImage };
|