embroidery-qc-image 1.0.24 → 1.0.25
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.d.ts +5 -0
- package/dist/index.esm.js +193 -56
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +193 -56
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAibD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAugCF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ interface TextPosition {
|
|
|
13
13
|
interface IconPosition {
|
|
14
14
|
type: 'ICON';
|
|
15
15
|
icon: number;
|
|
16
|
+
icon_name?: string | null;
|
|
17
|
+
icon_image?: string | null;
|
|
18
|
+
is_delete_icon?: boolean | null;
|
|
16
19
|
note?: string | null;
|
|
17
20
|
color?: string | null;
|
|
18
21
|
layer_colors: string[];
|
|
@@ -20,10 +23,12 @@ interface IconPosition {
|
|
|
20
23
|
type Position = TextPosition | IconPosition;
|
|
21
24
|
interface Side {
|
|
22
25
|
print_side: string;
|
|
26
|
+
item_type?: string | null;
|
|
23
27
|
positions: Position[];
|
|
24
28
|
}
|
|
25
29
|
interface EmbroideryQCConfig {
|
|
26
30
|
image_url?: string;
|
|
31
|
+
message?: string | null;
|
|
27
32
|
error_message?: string | null;
|
|
28
33
|
warning_message?: string | null;
|
|
29
34
|
sides: Side[];
|
package/dist/index.esm.js
CHANGED
|
@@ -46,7 +46,7 @@ const LAYOUT = {
|
|
|
46
46
|
// Font sizes (base values, will be multiplied by scaleFactor)
|
|
47
47
|
HEADER_FONT_SIZE: 220,
|
|
48
48
|
TEXT_FONT_SIZE: 200,
|
|
49
|
-
OTHER_FONT_SIZE:
|
|
49
|
+
OTHER_FONT_SIZE: 180,
|
|
50
50
|
// Colors
|
|
51
51
|
HEADER_COLOR: "#000000",
|
|
52
52
|
LABEL_COLOR: "#444444",
|
|
@@ -55,12 +55,12 @@ const LAYOUT = {
|
|
|
55
55
|
TEXT_ALIGN: "left",
|
|
56
56
|
TEXT_BASELINE: "top",
|
|
57
57
|
// Spacing
|
|
58
|
-
LINE_GAP:
|
|
59
|
-
PADDING:
|
|
58
|
+
LINE_GAP: 50,
|
|
59
|
+
PADDING: 50,
|
|
60
60
|
SECTION_SPACING: 60,
|
|
61
61
|
ELEMENT_SPACING: 100,
|
|
62
62
|
SWATCH_SPACING: 25,
|
|
63
|
-
FLORAL_SPACING:
|
|
63
|
+
FLORAL_SPACING: 100,
|
|
64
64
|
// Visual styling
|
|
65
65
|
SWATCH_HEIGHT_RATIO: 2.025,
|
|
66
66
|
UNDERLINE_POSITION: 0.9,
|
|
@@ -103,6 +103,17 @@ const getImageUrl = (type, value) => {
|
|
|
103
103
|
return `${BASE_URLS.THREAD_COLOR}/${value}.webp`;
|
|
104
104
|
};
|
|
105
105
|
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
106
|
+
const getIconImageUrl = (position) => {
|
|
107
|
+
if (position.is_delete_icon)
|
|
108
|
+
return null;
|
|
109
|
+
if (position.icon_image && position.icon_image.trim().length > 0) {
|
|
110
|
+
return position.icon_image;
|
|
111
|
+
}
|
|
112
|
+
if (position.icon !== 0) {
|
|
113
|
+
return getImageUrl("icon", position.icon);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
106
117
|
const ensureImage = (existing) => {
|
|
107
118
|
if (existing && existing.crossOrigin === "anonymous") {
|
|
108
119
|
return existing;
|
|
@@ -134,14 +145,14 @@ const loadImage = (url, imageRefs, onLoad) => {
|
|
|
134
145
|
img.onerror = () => {
|
|
135
146
|
if (!attemptedProxy) {
|
|
136
147
|
attemptedProxy = true;
|
|
137
|
-
img.src = getProxyUrl(url);
|
|
148
|
+
img.src = getProxyUrl(getResizeUrl(url));
|
|
138
149
|
return;
|
|
139
150
|
}
|
|
140
151
|
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
141
152
|
cleanup();
|
|
142
153
|
onLoad();
|
|
143
154
|
};
|
|
144
|
-
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
155
|
+
img.src = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
145
156
|
};
|
|
146
157
|
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
147
158
|
const key = cacheKey ?? url;
|
|
@@ -179,13 +190,13 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
179
190
|
target.onerror = () => {
|
|
180
191
|
if (!attemptedProxy) {
|
|
181
192
|
attemptedProxy = true;
|
|
182
|
-
target.src = getProxyUrl(url);
|
|
193
|
+
target.src = getProxyUrl(getResizeUrl(url));
|
|
183
194
|
return;
|
|
184
195
|
}
|
|
185
196
|
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
186
197
|
finalize();
|
|
187
198
|
};
|
|
188
|
-
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
199
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
|
|
189
200
|
if (target.src !== desiredSrc) {
|
|
190
201
|
target.src = desiredSrc;
|
|
191
202
|
}
|
|
@@ -194,6 +205,57 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
|
194
205
|
}
|
|
195
206
|
});
|
|
196
207
|
};
|
|
208
|
+
const getResizeUrl = (url) => {
|
|
209
|
+
try {
|
|
210
|
+
const urlObj = new URL(url);
|
|
211
|
+
// Xử lý cdn.shopify.com
|
|
212
|
+
if (urlObj.hostname === 'cdn.shopify.com') {
|
|
213
|
+
// Set hoặc update query param width=400
|
|
214
|
+
urlObj.searchParams.set('width', '400');
|
|
215
|
+
return urlObj.toString();
|
|
216
|
+
}
|
|
217
|
+
// Xử lý m.media-amazon.com
|
|
218
|
+
if (urlObj.hostname === 'm.media-amazon.com') {
|
|
219
|
+
const pathname = urlObj.pathname;
|
|
220
|
+
// Split pathname theo dấu /
|
|
221
|
+
const pathArr = pathname.split('/');
|
|
222
|
+
// Lấy filename (phần cuối cùng)
|
|
223
|
+
const filename = pathArr[pathArr.length - 1];
|
|
224
|
+
// Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
|
|
225
|
+
const cleanedFilename = filename.replace(/\._.*_/g, '');
|
|
226
|
+
// Split filename đã clean theo dấu .
|
|
227
|
+
const parts = cleanedFilename.split('.');
|
|
228
|
+
if (parts.length >= 2) {
|
|
229
|
+
// Lấy phần đầu và phần cuối
|
|
230
|
+
const firstPart = parts[0];
|
|
231
|
+
const lastPart = parts[parts.length - 1];
|
|
232
|
+
// Chèn _AC_SX400_ vào giữa và join lại
|
|
233
|
+
const newFilename = `${firstPart}._AC_SX400_.${lastPart}`;
|
|
234
|
+
// Thay filename mới vào pathArr
|
|
235
|
+
pathArr[pathArr.length - 1] = newFilename;
|
|
236
|
+
// Join lại
|
|
237
|
+
urlObj.pathname = pathArr.join('/');
|
|
238
|
+
return urlObj.toString();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Xử lý i.etsystatic.com
|
|
242
|
+
if (urlObj.hostname === 'i.etsystatic.com') {
|
|
243
|
+
const pathname = urlObj.pathname;
|
|
244
|
+
// Thay il_fullxfull bằng il_400x400
|
|
245
|
+
if (pathname.includes('il_fullxfull')) {
|
|
246
|
+
const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
|
|
247
|
+
urlObj.pathname = newPathname;
|
|
248
|
+
return urlObj.toString();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Nếu không phải các domain cần xử lý, return URL gốc
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// Nếu URL không hợp lệ, return URL gốc
|
|
256
|
+
return url;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
197
259
|
const preloadFonts = async (config) => {
|
|
198
260
|
if (config.error_message || !config.sides?.length)
|
|
199
261
|
return;
|
|
@@ -223,12 +285,10 @@ const preloadImages = async (config, imageRefs) => {
|
|
|
223
285
|
config.sides.forEach((side) => {
|
|
224
286
|
side.positions.forEach((position) => {
|
|
225
287
|
if (position.type === "ICON") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
seen.add(iconUrl);
|
|
231
|
-
}
|
|
288
|
+
const iconUrl = getIconImageUrl(position);
|
|
289
|
+
if (iconUrl && !seen.has(iconUrl)) {
|
|
290
|
+
entries.push({ url: iconUrl });
|
|
291
|
+
seen.add(iconUrl);
|
|
232
292
|
}
|
|
233
293
|
if (position.color) {
|
|
234
294
|
const threadUrl = getImageUrl("threadColor", position.color);
|
|
@@ -301,23 +361,30 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
301
361
|
};
|
|
302
362
|
};
|
|
303
363
|
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
currentLine = words[i];
|
|
364
|
+
// Mỗi '\n' tương đương với một line break giống như khi wrap tự động.
|
|
365
|
+
const segments = text.split("\n");
|
|
366
|
+
const result = [];
|
|
367
|
+
segments.forEach((segment) => {
|
|
368
|
+
const words = segment.split(" ").filter((word) => word.length > 0);
|
|
369
|
+
if (words.length === 0) {
|
|
370
|
+
// Nếu đoạn rỗng, thêm một dòng trống (break đúng 1 line)
|
|
371
|
+
result.push("");
|
|
372
|
+
return;
|
|
314
373
|
}
|
|
315
|
-
|
|
316
|
-
|
|
374
|
+
let currentLine = words[0];
|
|
375
|
+
for (let i = 1; i < words.length; i++) {
|
|
376
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
377
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
378
|
+
result.push(currentLine);
|
|
379
|
+
currentLine = words[i];
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
currentLine = testLine;
|
|
383
|
+
}
|
|
317
384
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return
|
|
385
|
+
result.push(currentLine);
|
|
386
|
+
});
|
|
387
|
+
return result.length ? result : [""];
|
|
321
388
|
};
|
|
322
389
|
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
323
390
|
let swatchX = startX;
|
|
@@ -381,8 +448,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
381
448
|
config.sides.forEach((side) => {
|
|
382
449
|
side.positions.forEach((position) => {
|
|
383
450
|
if (position.type === "ICON") {
|
|
384
|
-
|
|
385
|
-
|
|
451
|
+
const iconUrl = getIconImageUrl(position);
|
|
452
|
+
if (iconUrl) {
|
|
453
|
+
loadImage(iconUrl, imageRefs, incrementCounter);
|
|
386
454
|
}
|
|
387
455
|
position.layer_colors?.forEach((color) => {
|
|
388
456
|
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
@@ -433,7 +501,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
433
501
|
return;
|
|
434
502
|
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
435
503
|
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
436
|
-
// Calculate warning height (with scaleFactor = 1 for measurement)
|
|
504
|
+
// Calculate warning & message height (with scaleFactor = 1 for measurement)
|
|
437
505
|
let warningHeight = 0;
|
|
438
506
|
let warningLineHeight = 0;
|
|
439
507
|
let warningLineCount = 0;
|
|
@@ -452,6 +520,24 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
452
520
|
warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
|
|
453
521
|
}
|
|
454
522
|
}
|
|
523
|
+
let messageHeight = 0;
|
|
524
|
+
let messageLineHeight = 0;
|
|
525
|
+
let messageLineCount = 0;
|
|
526
|
+
if (config.message) {
|
|
527
|
+
const measureMessageCanvas = document.createElement("canvas");
|
|
528
|
+
measureMessageCanvas.width = canvas.width;
|
|
529
|
+
measureMessageCanvas.height = canvas.height;
|
|
530
|
+
const measureMessageCtx = measureMessageCanvas.getContext("2d");
|
|
531
|
+
if (measureMessageCtx) {
|
|
532
|
+
measureMessageCtx.textAlign = "left";
|
|
533
|
+
measureMessageCtx.textBaseline = "top";
|
|
534
|
+
measureMessageCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
|
|
535
|
+
const messageLines = buildWrappedLines(measureMessageCtx, config.message.trim(), canvas.width - LAYOUT.PADDING * 4);
|
|
536
|
+
messageLineCount = messageLines.length;
|
|
537
|
+
messageLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
|
|
538
|
+
messageHeight = messageLineCount * messageLineHeight + LAYOUT.PADDING;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
455
541
|
if (config.image_url) {
|
|
456
542
|
const mockupImage = imageRefs.current.get(config.image_url);
|
|
457
543
|
if (mockupImage) {
|
|
@@ -484,27 +570,40 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
|
484
570
|
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
485
571
|
let measureY = LAYOUT.PADDING;
|
|
486
572
|
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
487
|
-
// Add warning text height (without bottom padding, no spacing)
|
|
573
|
+
// Add warning & message text height (without bottom padding, no spacing)
|
|
488
574
|
if (config.warning_message) {
|
|
489
575
|
const warningTextHeight = warningHeight - LAYOUT.PADDING;
|
|
490
576
|
measureY += warningTextHeight;
|
|
491
577
|
}
|
|
578
|
+
if (config.message) {
|
|
579
|
+
const messageTextHeight = messageHeight - LAYOUT.PADDING;
|
|
580
|
+
measureY += messageTextHeight;
|
|
581
|
+
}
|
|
492
582
|
config.sides.forEach((side) => {
|
|
493
583
|
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
494
584
|
measureY += sideHeight + measureSpacing;
|
|
495
585
|
});
|
|
496
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight) / measureY));
|
|
586
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
|
|
497
587
|
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
498
|
-
// Render warning with scaleFactor and get actual
|
|
588
|
+
// Render warning & message with scaleFactor and get actual heights
|
|
499
589
|
let actualWarningHeight = 0;
|
|
590
|
+
let actualMessageHeight = 0;
|
|
500
591
|
if (config.warning_message) {
|
|
501
592
|
actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
|
|
502
593
|
}
|
|
503
|
-
|
|
594
|
+
if (config.message) {
|
|
595
|
+
actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, actualWarningHeight, "", // message: không cần prefix "Note"
|
|
596
|
+
DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
// Calculate currentY: padding top + actual warning & message height (no spacing)
|
|
504
600
|
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
505
601
|
if (config.warning_message && actualWarningHeight > 0) {
|
|
506
602
|
currentY += actualWarningHeight;
|
|
507
603
|
}
|
|
604
|
+
if (config.message && actualMessageHeight > 0) {
|
|
605
|
+
currentY += actualMessageHeight;
|
|
606
|
+
}
|
|
508
607
|
config.sides.forEach((side) => {
|
|
509
608
|
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
510
609
|
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
@@ -546,8 +645,8 @@ const renderErrorState = (ctx, canvas, message) => {
|
|
|
546
645
|
});
|
|
547
646
|
ctx.restore();
|
|
548
647
|
};
|
|
549
|
-
const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
550
|
-
const sanitizedMessage =
|
|
648
|
+
const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefix = "Note: ", color = DEFAULT_WARNING_COLOR) => {
|
|
649
|
+
const sanitizedMessage = `${prefix}${message.trim()}`;
|
|
551
650
|
const horizontalPadding = LAYOUT.PADDING * 2 * scaleFactor;
|
|
552
651
|
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
553
652
|
const baseFontSize = LAYOUT.HEADER_FONT_SIZE * 0.7 * scaleFactor;
|
|
@@ -556,7 +655,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
556
655
|
ctx.save();
|
|
557
656
|
ctx.textAlign = "left";
|
|
558
657
|
ctx.textBaseline = "top";
|
|
559
|
-
ctx.fillStyle =
|
|
658
|
+
ctx.fillStyle = color;
|
|
560
659
|
ctx.font = `${baseFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
561
660
|
let fontSize = baseFontSize;
|
|
562
661
|
let lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
@@ -575,7 +674,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
|
|
|
575
674
|
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
576
675
|
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
577
676
|
}
|
|
578
|
-
const startY = LAYOUT.PADDING * scaleFactor;
|
|
677
|
+
const startY = LAYOUT.PADDING * scaleFactor + offsetY;
|
|
579
678
|
lines.forEach((line, index) => {
|
|
580
679
|
const y = startY + index * lineHeight;
|
|
581
680
|
ctx.fillText(line, leftX, y);
|
|
@@ -599,7 +698,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
|
599
698
|
ctx.drawImage(mockupImg, x, y, width, height);
|
|
600
699
|
// Draw florals
|
|
601
700
|
if (floralAssets.length > 0) {
|
|
602
|
-
const floralH = Math.min(
|
|
701
|
+
const floralH = Math.min(500, height);
|
|
603
702
|
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
604
703
|
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
605
704
|
const img = floralAssets[i];
|
|
@@ -928,21 +1027,56 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
928
1027
|
return drawnHeight;
|
|
929
1028
|
};
|
|
930
1029
|
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
|
|
931
|
-
|
|
1030
|
+
// Dùng cùng font size với Text cho label và value icon
|
|
1031
|
+
const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
932
1032
|
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
933
1033
|
ctx.save();
|
|
934
|
-
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
935
1034
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
936
1035
|
let cursorY = y;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1036
|
+
// Tách label "Icon:" (in đậm) và phần value (thường)
|
|
1037
|
+
const iconLabel = "Icon:";
|
|
1038
|
+
let iconValue;
|
|
1039
|
+
if (position.is_delete_icon) {
|
|
1040
|
+
// Ưu tiên hiển thị không có icon nếu được đánh dấu xóa
|
|
1041
|
+
iconValue = "(không có icon)";
|
|
1042
|
+
}
|
|
1043
|
+
else if (position.note) {
|
|
1044
|
+
iconValue = position.note;
|
|
1045
|
+
}
|
|
1046
|
+
else if (position.icon_name && position.icon_name.trim().length > 0) {
|
|
1047
|
+
// Nếu có icon_name thì hiển thị tên đó
|
|
1048
|
+
iconValue = position.icon_name;
|
|
1049
|
+
}
|
|
1050
|
+
else if (position.icon === 0) {
|
|
1051
|
+
// Icon mặc định theo file thêu
|
|
1052
|
+
iconValue = "(icon mặc định theo file thêu)";
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
// Fallback: hiển thị mã icon (ép sang string)
|
|
1056
|
+
iconValue = String(position.icon);
|
|
1057
|
+
}
|
|
1058
|
+
// Vẽ label Icon: (bold, màu label mặc định)
|
|
1059
|
+
ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1060
|
+
ctx.fillText(iconLabel, x, cursorY);
|
|
1061
|
+
const labelWidth = ctx.measureText(iconLabel).width;
|
|
1062
|
+
// Vẽ value kế bên, font thường, màu đỏ giống text value
|
|
1063
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
1064
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
1065
|
+
const valueText = ` ${iconValue}`;
|
|
1066
|
+
ctx.fillText(valueText, x + labelWidth, cursorY);
|
|
1067
|
+
// Reset lại màu về label color cho các phần text tiếp theo (màu chỉ, v.v.)
|
|
1068
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
1069
|
+
const valueWidth = ctx.measureText(valueText).width;
|
|
1070
|
+
const iconResult = {
|
|
1071
|
+
height: iconFontSize + lineGap,
|
|
1072
|
+
// tổng width của cả label + value, dùng để canh icon image lệch sang phải
|
|
1073
|
+
lastLineWidth: labelWidth + valueWidth,
|
|
1074
|
+
lastLineY: cursorY,
|
|
1075
|
+
};
|
|
942
1076
|
// Draw icon image
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const img = imageRefs.current.get(
|
|
1077
|
+
const iconUrl = getIconImageUrl(position);
|
|
1078
|
+
if (iconUrl) {
|
|
1079
|
+
const img = imageRefs.current.get(iconUrl);
|
|
946
1080
|
if (img?.complete && img.naturalHeight > 0) {
|
|
947
1081
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
948
1082
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
@@ -955,11 +1089,14 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
955
1089
|
}
|
|
956
1090
|
cursorY += iconResult.height;
|
|
957
1091
|
// Draw color swatches (prefer layer_colors, fallback to single color)
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1092
|
+
// Nếu icon đã bị xóa thì không cần hiển thị màu chỉ nữa
|
|
1093
|
+
const iconColors = position.is_delete_icon
|
|
1094
|
+
? null
|
|
1095
|
+
: position.layer_colors?.length
|
|
1096
|
+
? position.layer_colors
|
|
1097
|
+
: position.color
|
|
1098
|
+
? [position.color]
|
|
1099
|
+
: null;
|
|
963
1100
|
const layerCount = position.layer_colors?.length ?? 0;
|
|
964
1101
|
const hasMultiLayerColors = layerCount > 1;
|
|
965
1102
|
const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;
|