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