embroidery-qc-image 1.0.7 → 1.0.8
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/README.md +61 -84
- package/dist/components/EmbroideryQCImage.d.ts +9 -1
- package/dist/components/EmbroideryQCImage.d.ts.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +407 -112
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +408 -111
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -35,37 +35,68 @@ styleInject(css_248z);
|
|
|
35
35
|
// CONSTANTS
|
|
36
36
|
// ============================================================================
|
|
37
37
|
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
|
-
|
|
38
|
+
"Army (1394)": "#4B5320",
|
|
39
|
+
Army: "#4B5320",
|
|
40
|
+
"Black (8)": "#000000",
|
|
41
|
+
Black: "#000000",
|
|
42
|
+
"Bubblegum (1309)": "#FFC1CC",
|
|
43
|
+
Bubblegum: "#FFC1CC",
|
|
44
|
+
"Carolina Blue (1274)": "#7BAFD4",
|
|
45
|
+
"Carolina Blue": "#7BAFD4",
|
|
46
|
+
"Celadon (1098)": "#ACE1AF",
|
|
47
|
+
Celadon: "#ACE1AF",
|
|
48
|
+
"Coffee Bean (1145)": "#6F4E37",
|
|
49
|
+
"Coffee Bean": "#6F4E37",
|
|
50
|
+
"Daffodil (1180)": "#FFFF31",
|
|
51
|
+
Daffodil: "#FFFF31",
|
|
52
|
+
"Dark Gray (1131)": "#A9A9A9",
|
|
53
|
+
"Dark Gray": "#A9A9A9",
|
|
54
|
+
"Doe Skin Beige (1344)": "#F5E6D3",
|
|
55
|
+
"Doe Skin Beige": "#F5E6D3",
|
|
56
|
+
"Dusty Blue (1373)": "#6699CC",
|
|
57
|
+
"Dusty Blue": "#6699CC",
|
|
58
|
+
"Forest Green (1397)": "#228B22",
|
|
59
|
+
"Forest Green": "#228B22",
|
|
60
|
+
"Gold (1425)": "#FFD700",
|
|
61
|
+
Gold: "#FFD700",
|
|
62
|
+
"Gray (1118)": "#808080",
|
|
63
|
+
Gray: "#808080",
|
|
64
|
+
"Ivory (1072)": "#FFFFF0",
|
|
65
|
+
Ivory: "#FFFFF0",
|
|
66
|
+
"Lavender (1032)": "#E6E6FA",
|
|
67
|
+
Lavender: "#E6E6FA",
|
|
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",
|
|
68
98
|
};
|
|
99
|
+
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
69
100
|
const BASE_URLS = {
|
|
70
101
|
FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
|
|
71
102
|
ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
|
|
@@ -128,14 +159,170 @@ const getImageUrl = (type, value) => {
|
|
|
128
159
|
return `${BASE_URLS.FLORAL}/${value}.png`;
|
|
129
160
|
return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
|
|
130
161
|
};
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
163
|
+
const ensureImage = (existing) => {
|
|
164
|
+
if (existing && existing.crossOrigin === "anonymous") {
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
134
167
|
const img = new Image();
|
|
135
168
|
img.crossOrigin = "anonymous";
|
|
136
|
-
img.
|
|
137
|
-
img
|
|
169
|
+
img.decoding = "async";
|
|
170
|
+
return img;
|
|
171
|
+
};
|
|
172
|
+
const loadImage = (url, imageRefs, onLoad) => {
|
|
173
|
+
const existing = imageRefs.current.get(url);
|
|
174
|
+
if (existing?.complete &&
|
|
175
|
+
existing.naturalWidth > 0 &&
|
|
176
|
+
existing.crossOrigin === "anonymous") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const img = ensureImage(existing);
|
|
138
180
|
imageRefs.current.set(url, img);
|
|
181
|
+
let attemptedProxy = existing?.dataset?.proxyUsed === "true";
|
|
182
|
+
const cleanup = () => {
|
|
183
|
+
img.onload = null;
|
|
184
|
+
img.onerror = null;
|
|
185
|
+
};
|
|
186
|
+
img.onload = () => {
|
|
187
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
188
|
+
cleanup();
|
|
189
|
+
onLoad();
|
|
190
|
+
};
|
|
191
|
+
img.onerror = () => {
|
|
192
|
+
if (!attemptedProxy) {
|
|
193
|
+
attemptedProxy = true;
|
|
194
|
+
img.src = getProxyUrl(url);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
198
|
+
cleanup();
|
|
199
|
+
onLoad();
|
|
200
|
+
};
|
|
201
|
+
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
202
|
+
};
|
|
203
|
+
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
204
|
+
const key = cacheKey ?? url;
|
|
205
|
+
const existing = imageRefs.current.get(key) ?? imageRefs.current.get(url);
|
|
206
|
+
if (existing?.complete &&
|
|
207
|
+
existing.naturalWidth > 0 &&
|
|
208
|
+
existing.crossOrigin === "anonymous" &&
|
|
209
|
+
existing.dataset?.proxyUsed !== undefined) {
|
|
210
|
+
if (existing !== imageRefs.current.get(key)) {
|
|
211
|
+
imageRefs.current.set(key, existing);
|
|
212
|
+
}
|
|
213
|
+
if (existing !== imageRefs.current.get(url)) {
|
|
214
|
+
imageRefs.current.set(url, existing);
|
|
215
|
+
}
|
|
216
|
+
return Promise.resolve(existing);
|
|
217
|
+
}
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const target = ensureImage(existing);
|
|
220
|
+
if (target !== existing) {
|
|
221
|
+
imageRefs.current.set(key, target);
|
|
222
|
+
imageRefs.current.set(url, target);
|
|
223
|
+
}
|
|
224
|
+
let attemptedProxy = target.dataset.proxyUsed === "true";
|
|
225
|
+
const finalize = () => {
|
|
226
|
+
target.onload = null;
|
|
227
|
+
target.onerror = null;
|
|
228
|
+
if (target.complete && target.naturalWidth > 0) {
|
|
229
|
+
imageRefs.current.set(key, target);
|
|
230
|
+
imageRefs.current.set(url, target);
|
|
231
|
+
}
|
|
232
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
233
|
+
resolve(target);
|
|
234
|
+
};
|
|
235
|
+
target.onload = finalize;
|
|
236
|
+
target.onerror = () => {
|
|
237
|
+
if (!attemptedProxy) {
|
|
238
|
+
attemptedProxy = true;
|
|
239
|
+
target.src = getProxyUrl(url);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
243
|
+
finalize();
|
|
244
|
+
};
|
|
245
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
246
|
+
if (target.src !== desiredSrc) {
|
|
247
|
+
target.src = desiredSrc;
|
|
248
|
+
}
|
|
249
|
+
else if (target.complete && target.naturalWidth > 0) {
|
|
250
|
+
finalize();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
const preloadFonts = async (config) => {
|
|
255
|
+
if (config.error || !config.sides?.length)
|
|
256
|
+
return;
|
|
257
|
+
const fonts = new Set();
|
|
258
|
+
config.sides.forEach((side) => {
|
|
259
|
+
side.positions.forEach((position) => {
|
|
260
|
+
if (position.type === "TEXT" && position.font) {
|
|
261
|
+
fonts.add(position.font);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
if (fonts.size === 0)
|
|
266
|
+
return;
|
|
267
|
+
await Promise.all([...fonts].map((font) => loadFont(font)));
|
|
268
|
+
};
|
|
269
|
+
const preloadImages = async (config, imageRefs) => {
|
|
270
|
+
const entries = [];
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
if (config.image_url) {
|
|
273
|
+
entries.push({ url: config.image_url, cacheKey: "mockup" });
|
|
274
|
+
seen.add(config.image_url);
|
|
275
|
+
}
|
|
276
|
+
if (!config.sides?.length) {
|
|
277
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
config.sides.forEach((side) => {
|
|
281
|
+
side.positions.forEach((position) => {
|
|
282
|
+
if (position.type === "ICON") {
|
|
283
|
+
if (position.icon !== 0) {
|
|
284
|
+
const iconUrl = getImageUrl("icon", position.icon);
|
|
285
|
+
if (!seen.has(iconUrl)) {
|
|
286
|
+
entries.push({ url: iconUrl });
|
|
287
|
+
seen.add(iconUrl);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
position.layer_colors?.forEach((color) => {
|
|
291
|
+
const colorUrl = getImageUrl("threadColor", color);
|
|
292
|
+
if (!seen.has(colorUrl)) {
|
|
293
|
+
entries.push({ url: colorUrl });
|
|
294
|
+
seen.add(colorUrl);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (position.type === "TEXT") {
|
|
299
|
+
if (position.floral_pattern) {
|
|
300
|
+
const floralUrl = getImageUrl("floral", position.floral_pattern);
|
|
301
|
+
if (!seen.has(floralUrl)) {
|
|
302
|
+
entries.push({ url: floralUrl });
|
|
303
|
+
seen.add(floralUrl);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (position.color) {
|
|
307
|
+
const threadUrl = getImageUrl("threadColor", position.color);
|
|
308
|
+
if (!seen.has(threadUrl)) {
|
|
309
|
+
entries.push({ url: threadUrl });
|
|
310
|
+
seen.add(threadUrl);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
position.character_colors?.forEach((color) => {
|
|
314
|
+
const characterColorUrl = getImageUrl("threadColor", color);
|
|
315
|
+
if (!seen.has(characterColorUrl)) {
|
|
316
|
+
entries.push({ url: characterColorUrl });
|
|
317
|
+
seen.add(characterColorUrl);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
if (entries.length === 0)
|
|
324
|
+
return;
|
|
325
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
139
326
|
};
|
|
140
327
|
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
141
328
|
const words = text.split(" ");
|
|
@@ -163,6 +350,25 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
163
350
|
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
164
351
|
};
|
|
165
352
|
};
|
|
353
|
+
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
354
|
+
const words = text.split(" ").filter((word) => word.length > 0);
|
|
355
|
+
if (words.length === 0)
|
|
356
|
+
return [""];
|
|
357
|
+
const lines = [];
|
|
358
|
+
let currentLine = words[0];
|
|
359
|
+
for (let i = 1; i < words.length; i++) {
|
|
360
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
361
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
362
|
+
lines.push(currentLine);
|
|
363
|
+
currentLine = words[i];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
currentLine = testLine;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push(currentLine);
|
|
370
|
+
return lines;
|
|
371
|
+
};
|
|
166
372
|
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
167
373
|
const words = text.split(" ");
|
|
168
374
|
const lines = [];
|
|
@@ -225,7 +431,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
225
431
|
// Load fonts
|
|
226
432
|
useEffect(() => {
|
|
227
433
|
const loadFonts = async () => {
|
|
228
|
-
if (!config.sides?.length)
|
|
434
|
+
if (config.error || !config.sides?.length)
|
|
229
435
|
return;
|
|
230
436
|
const fontsToLoad = new Set();
|
|
231
437
|
config.sides.forEach((side) => {
|
|
@@ -251,23 +457,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
251
457
|
}, [config.sides, loadedFonts]);
|
|
252
458
|
// Load images
|
|
253
459
|
useEffect(() => {
|
|
254
|
-
if (!config.sides?.length)
|
|
460
|
+
if (config.error || !config.sides?.length)
|
|
255
461
|
return;
|
|
256
462
|
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
257
463
|
// Load mockup
|
|
258
464
|
if (config.image_url) {
|
|
259
|
-
|
|
260
|
-
const img = new Image();
|
|
261
|
-
if (useCors)
|
|
262
|
-
img.crossOrigin = "anonymous";
|
|
263
|
-
img.onload = () => {
|
|
264
|
-
imageRefs.current.set("mockup", img);
|
|
265
|
-
incrementCounter();
|
|
266
|
-
};
|
|
267
|
-
img.onerror = () => useCors && loadMockup(false);
|
|
268
|
-
img.src = config.image_url;
|
|
269
|
-
};
|
|
270
|
-
loadMockup(true);
|
|
465
|
+
loadImage(config.image_url, imageRefs, incrementCounter);
|
|
271
466
|
}
|
|
272
467
|
// Load all other images
|
|
273
468
|
config.sides.forEach((side) => {
|
|
@@ -297,61 +492,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
297
492
|
// Render canvas
|
|
298
493
|
useEffect(() => {
|
|
299
494
|
const renderCanvas = () => {
|
|
300
|
-
if (!canvasRef.current
|
|
495
|
+
if (!canvasRef.current)
|
|
301
496
|
return;
|
|
302
|
-
|
|
303
|
-
const ctx = canvas.getContext("2d");
|
|
304
|
-
if (!ctx)
|
|
305
|
-
return;
|
|
306
|
-
canvas.width = canvasSize.width;
|
|
307
|
-
canvas.height = canvasSize.height;
|
|
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;
|
|
313
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
314
|
-
// Collect floral assets
|
|
315
|
-
const floralAssets = [];
|
|
316
|
-
const seenFlorals = new Set();
|
|
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);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
// Calculate scale factor
|
|
332
|
-
const measureCanvas = document.createElement("canvas");
|
|
333
|
-
measureCanvas.width = canvas.width;
|
|
334
|
-
measureCanvas.height = canvas.height;
|
|
335
|
-
const measureCtx = measureCanvas.getContext("2d");
|
|
336
|
-
if (!measureCtx)
|
|
337
|
-
return;
|
|
338
|
-
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
339
|
-
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
340
|
-
let measureY = LAYOUT.PADDING;
|
|
341
|
-
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
342
|
-
config.sides.forEach((side) => {
|
|
343
|
-
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
344
|
-
measureY += sideHeight + measureSpacing;
|
|
345
|
-
});
|
|
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;
|
|
351
|
-
config.sides.forEach((side) => {
|
|
352
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
353
|
-
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
354
|
-
});
|
|
497
|
+
renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
|
|
355
498
|
};
|
|
356
499
|
const timer = setTimeout(renderCanvas, 100);
|
|
357
500
|
return () => clearTimeout(timer);
|
|
@@ -361,6 +504,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
361
504
|
// ============================================================================
|
|
362
505
|
// RENDERING FUNCTIONS
|
|
363
506
|
// ============================================================================
|
|
507
|
+
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
508
|
+
const ctx = canvas.getContext("2d");
|
|
509
|
+
if (!ctx)
|
|
510
|
+
return;
|
|
511
|
+
canvas.width = canvasSize.width;
|
|
512
|
+
canvas.height = canvasSize.height;
|
|
513
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
514
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
515
|
+
if (config.error) {
|
|
516
|
+
renderErrorState(ctx, canvas, config.error);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!config.sides?.length)
|
|
520
|
+
return;
|
|
521
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
522
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
523
|
+
if (config.image_url) {
|
|
524
|
+
const mockupImage = imageRefs.current.get(config.image_url);
|
|
525
|
+
if (mockupImage) {
|
|
526
|
+
imageRefs.current.set("mockup", mockupImage);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const floralAssets = [];
|
|
530
|
+
const seenFlorals = new Set();
|
|
531
|
+
config.sides.forEach((side) => {
|
|
532
|
+
side.positions.forEach((position) => {
|
|
533
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
534
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
535
|
+
if (!seenFlorals.has(url)) {
|
|
536
|
+
const img = imageRefs.current.get(url);
|
|
537
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
538
|
+
floralAssets.push(img);
|
|
539
|
+
seenFlorals.add(url);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
const measureCanvas = document.createElement("canvas");
|
|
546
|
+
measureCanvas.width = canvas.width;
|
|
547
|
+
measureCanvas.height = canvas.height;
|
|
548
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
549
|
+
if (!measureCtx)
|
|
550
|
+
return;
|
|
551
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
552
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
553
|
+
let measureY = LAYOUT.PADDING;
|
|
554
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
555
|
+
config.sides.forEach((side) => {
|
|
556
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
557
|
+
measureY += sideHeight + measureSpacing;
|
|
558
|
+
});
|
|
559
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
560
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
561
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
562
|
+
config.sides.forEach((side) => {
|
|
563
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
564
|
+
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
const renderErrorState = (ctx, canvas, message) => {
|
|
568
|
+
const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
|
|
569
|
+
const horizontalPadding = LAYOUT.PADDING * 3;
|
|
570
|
+
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
571
|
+
const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
|
|
572
|
+
const minFontSize = 60;
|
|
573
|
+
const centerX = canvas.width / 2;
|
|
574
|
+
ctx.save();
|
|
575
|
+
ctx.textAlign = "center";
|
|
576
|
+
ctx.textBaseline = "top";
|
|
577
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
578
|
+
let fontSize = baseFontSize;
|
|
579
|
+
let lineGap = LAYOUT.LINE_GAP;
|
|
580
|
+
let lineHeight = fontSize + lineGap;
|
|
581
|
+
const adjustMetrics = () => {
|
|
582
|
+
ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
583
|
+
lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
|
|
584
|
+
lineHeight = fontSize + lineGap;
|
|
585
|
+
};
|
|
586
|
+
adjustMetrics();
|
|
587
|
+
let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
588
|
+
let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
589
|
+
while (longestLineWidth > maxWidth && fontSize > minFontSize) {
|
|
590
|
+
fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
|
|
591
|
+
adjustMetrics();
|
|
592
|
+
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
593
|
+
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
594
|
+
}
|
|
595
|
+
const totalHeight = lines.length * lineHeight;
|
|
596
|
+
const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
|
|
597
|
+
lines.forEach((line, index) => {
|
|
598
|
+
const y = startY + index * lineHeight;
|
|
599
|
+
ctx.fillText(line, centerX, y);
|
|
600
|
+
});
|
|
601
|
+
ctx.restore();
|
|
602
|
+
};
|
|
364
603
|
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
365
604
|
const mockupImg = imageRefs.current.get("mockup");
|
|
366
605
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
@@ -446,7 +685,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
446
685
|
side.positions.forEach((position) => {
|
|
447
686
|
if (position.type === "ICON") {
|
|
448
687
|
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
449
|
-
currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
|
|
688
|
+
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
450
689
|
}
|
|
451
690
|
});
|
|
452
691
|
return currentY - startY;
|
|
@@ -492,7 +731,9 @@ const computeUniformProperties = (textPositions) => {
|
|
|
492
731
|
const fonts = new Set(textPositions.map((p) => p.font));
|
|
493
732
|
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
494
733
|
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
495
|
-
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
734
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
735
|
+
? p.character_colors.join(",")
|
|
736
|
+
: p.color ?? "None"));
|
|
496
737
|
return {
|
|
497
738
|
values: {
|
|
498
739
|
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
@@ -531,9 +772,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
531
772
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
532
773
|
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
533
774
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
534
|
-
const swatchX = x +
|
|
775
|
+
const swatchX = x +
|
|
776
|
+
Math.ceil(result.lastLineWidth) +
|
|
777
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
535
778
|
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
536
|
-
const colors = values.color.includes(",")
|
|
779
|
+
const colors = values.color.includes(",")
|
|
780
|
+
? values.color.split(",").map((s) => s.trim())
|
|
781
|
+
: [values.color];
|
|
537
782
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
538
783
|
cursorY += result.height;
|
|
539
784
|
rendered++;
|
|
@@ -605,7 +850,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
605
850
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
606
851
|
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
607
852
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
608
|
-
const swatchX = x +
|
|
853
|
+
const swatchX = x +
|
|
854
|
+
Math.ceil(result.lastLineWidth) +
|
|
855
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
609
856
|
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
610
857
|
const colors = position.character_colors || [position.color];
|
|
611
858
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -628,7 +875,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
628
875
|
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
629
876
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
630
877
|
let cursorY = y;
|
|
631
|
-
const iconText = position.icon === 0
|
|
878
|
+
const iconText = position.icon === 0
|
|
879
|
+
? `Icon: icon mặc định theo file thêu`
|
|
880
|
+
: `Icon: ${position.icon}`;
|
|
632
881
|
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
633
882
|
// Draw icon image
|
|
634
883
|
if (position.icon !== 0) {
|
|
@@ -637,7 +886,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
637
886
|
if (img?.complete && img.naturalHeight > 0) {
|
|
638
887
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
639
888
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
640
|
-
const iconX = x +
|
|
889
|
+
const iconX = x +
|
|
890
|
+
Math.ceil(iconResult.lastLineWidth) +
|
|
891
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
641
892
|
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
642
893
|
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
643
894
|
}
|
|
@@ -648,7 +899,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
648
899
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
649
900
|
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
650
901
|
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
651
|
-
const swatchX = x +
|
|
902
|
+
const swatchX = x +
|
|
903
|
+
Math.ceil(colorResult.lastLineWidth) +
|
|
904
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
652
905
|
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
653
906
|
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
654
907
|
cursorY += colorResult.height;
|
|
@@ -656,6 +909,48 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
656
909
|
ctx.restore();
|
|
657
910
|
return cursorY - y;
|
|
658
911
|
};
|
|
912
|
+
const prepareExportCanvas = async (config, options = {}) => {
|
|
913
|
+
const { width = 4200, height = 4800 } = options;
|
|
914
|
+
const canvas = document.createElement("canvas");
|
|
915
|
+
const imageRefs = {
|
|
916
|
+
current: new Map(),
|
|
917
|
+
};
|
|
918
|
+
await preloadFonts(config);
|
|
919
|
+
await preloadImages(config, imageRefs);
|
|
920
|
+
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
921
|
+
if (!canvas.width || !canvas.height) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
return canvas;
|
|
925
|
+
};
|
|
926
|
+
const generateEmbroideryQCImageBlob = async (config, options = {}) => {
|
|
927
|
+
if (typeof document === "undefined") {
|
|
928
|
+
throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
|
|
929
|
+
}
|
|
930
|
+
const { mimeType = "image/png", quality } = options;
|
|
931
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
932
|
+
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
const blob = await new Promise((resolve) => {
|
|
936
|
+
canvas.toBlob((result) => resolve(result), mimeType, quality);
|
|
937
|
+
});
|
|
938
|
+
return blob;
|
|
939
|
+
};
|
|
940
|
+
const generateEmbroideryQCImageData = async (config, options = {}) => {
|
|
941
|
+
if (typeof document === "undefined") {
|
|
942
|
+
throw new Error("generateEmbroideryQCImageData requires a browser environment.");
|
|
943
|
+
}
|
|
944
|
+
const { mimeType = "image/png", quality } = options;
|
|
945
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
946
|
+
if (!canvas) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
if (mimeType === "image/png" || typeof quality === "undefined") {
|
|
950
|
+
return canvas.toDataURL(mimeType);
|
|
951
|
+
}
|
|
952
|
+
return canvas.toDataURL(mimeType, quality);
|
|
953
|
+
};
|
|
659
954
|
|
|
660
|
-
export { EmbroideryQCImage };
|
|
955
|
+
export { EmbroideryQCImage, generateEmbroideryQCImageBlob, generateEmbroideryQCImageData };
|
|
661
956
|
//# sourceMappingURL=index.esm.js.map
|