Download src/lib/engine/imposition.ts from phucsd/PrintCal: direct link, hf CLI and curl.
- Browser
- Download file 23.3 kB
-
https://huggingface.co/spaces/phucsd/PrintCal/resolve/main/src/lib/engine/imposition.ts
- Command line
-
hf download hf://spaces/phucsd/PrintCal/src/lib/engine/imposition.ts
-
curl -L -o imposition.ts https://huggingface.co/spaces/phucsd/PrintCal/resolve/main/src/lib/engine/imposition.ts
23.3 kB
| import { BoxCoordinate, DigitalMode, ImpositionResult, PaperCutBlock, PaperCutLine, PaperCutScheme, PaperType, PrintTech } from '@/types'; | |
| interface ImpositionOptions { | |
| widthMm: number; | |
| heightMm: number; | |
| bleedMm?: number; | |
| paperType: PaperType; | |
| printTech: PrintTech; | |
| digitalMode?: DigitalMode; | |
| preferredMachineSheet?: { widthMm: number; heightMm: number; name: string }; | |
| gripperMarginMm?: number; | |
| } | |
| interface EvaluationCandidate { | |
| name: string; | |
| sheetWidthMm: number; | |
| sheetHeightMm: number; | |
| cutsFromParent: number; | |
| cutScheme: PaperCutScheme; | |
| gripperMarginMm: number; | |
| } | |
| export function calculateImposition({ | |
| widthMm, | |
| heightMm, | |
| bleedMm = 2, | |
| paperType, | |
| printTech, | |
| digitalMode = 'with_paper', | |
| preferredMachineSheet, | |
| gripperMarginMm = 10, | |
| }: ImpositionOptions): ImpositionResult { | |
| const parentW = paperType.parentWidthCm * 10; | |
| const parentH = paperType.parentHeightCm * 10; | |
| // 1. Sinh danh sách các phương án cắt xả giấy mẹ | |
| const candidates: EvaluationCandidate[] = []; | |
| if (preferredMachineSheet) { | |
| // Máy in cụ thể được chỉ định | |
| const cuts = calculateCutsFromParent(parentW, parentH, preferredMachineSheet.widthMm, preferredMachineSheet.heightMm); | |
| const cutScheme = determineCutScheme(parentW, parentH, preferredMachineSheet.widthMm, preferredMachineSheet.heightMm, cuts); | |
| candidates.push({ | |
| name: preferredMachineSheet.name, | |
| sheetWidthMm: preferredMachineSheet.widthMm, | |
| sheetHeightMm: preferredMachineSheet.heightMm, | |
| cutsFromParent: cuts, | |
| cutScheme, | |
| gripperMarginMm, | |
| }); | |
| } else if (printTech === 'digital') { | |
| // In nhanh KTS: Theo chuẩn Bảng Giá In Nhanh Kèm Giấy & In Gia Công Konica C12000 | |
| let digitalCandidates: { name: string; w: number; h: number; cuts: number }[] = []; | |
| if (digitalMode === 'with_paper') { | |
| // BẢNG GIÁ IN NHANH KÈM GIẤY: Khổ 325 x 430 mm (chính) & 325 x 355 mm (Fort) | |
| digitalCandidates = [ | |
| { name: 'Khổ in kèm giấy (325 x 430 mm)', w: 430, h: 325, cuts: 1 }, | |
| { name: 'Khổ in kèm giấy (325 x 355 mm)', w: 355, h: 325, cuts: 1 }, | |
| ]; | |
| } else { | |
| // BẢNG GIÁ IN GIA CÔNG KONICA C12000 / C12010S: 5 khổ in máy | |
| digitalCandidates = [ | |
| { name: 'Khổ Konica A4 chuẩn (210 x 297 mm)', w: 297, h: 210, cuts: 1 }, | |
| { name: 'Khổ Konica 330 x 355 mm', w: 355, h: 330, cuts: 1 }, | |
| { name: 'Khổ Konica A3 chuẩn (297 x 420 mm)', w: 420, h: 297, cuts: 1 }, | |
| { name: 'Khổ Konica A3+ (330 x 483 mm)', w: 483, h: 330, cuts: 1 }, | |
| { name: 'Khổ Konica Banner (330 x 1200 mm)', w: 1200, h: 330, cuts: 1 }, | |
| ]; | |
| } | |
| for (const dc of digitalCandidates) { | |
| const isWithPaper = digitalMode === 'with_paper'; | |
| const cutScheme: PaperCutScheme = isWithPaper | |
| ? { | |
| parentWidthMm: dc.w, | |
| parentHeightMm: dc.h, | |
| cutType: 'chia_1', | |
| cutDescription: 'Phôi giấy cắt sẵn khổ chuẩn của xưởng INTC (đã gồm trong đơn giá in, không cần xả từ khổ mẹ)', | |
| cutsCount: 1, | |
| cutSheetWidthMm: dc.w, | |
| cutSheetHeightMm: dc.h, | |
| wasteAreaPercent: 0, | |
| cutLines: [], | |
| blocks: [ | |
| { | |
| index: 1, | |
| x: 0, | |
| y: 0, | |
| w: dc.w, | |
| h: dc.h, | |
| name: `Phôi in trọn gói (${dc.w} × ${dc.h} mm)`, | |
| }, | |
| ], | |
| } | |
| : determineCutScheme(parentW, parentH, dc.w, dc.h, calculateCutsFromParent(parentW, parentH, dc.w, dc.h)); | |
| candidates.push({ | |
| name: dc.name, | |
| sheetWidthMm: dc.w, | |
| sheetHeightMm: dc.h, | |
| cutsFromParent: isWithPaper ? 1 : calculateCutsFromParent(parentW, parentH, dc.w, dc.h), | |
| cutScheme, | |
| gripperMarginMm: 2.5, | |
| }); | |
| } | |
| } else { | |
| // In Offset: Các phương án chia khổ giấy mẹ chuẩn công nghiệp | |
| // Phương án 0: Nguyên khổ (Chia 1) - ví dụ 65x86 (860 x 650 mm) | |
| if (Math.max(parentW, parentH) <= 920) { | |
| const w1 = Math.max(parentW, parentH); | |
| const h1 = Math.min(parentW, parentH); | |
| candidates.push({ | |
| name: `Khổ nguyên ${Math.round(parentW/10)}x${Math.round(parentH/10)}cm (Bắt nhíp ${w1/10}cm)`, | |
| sheetWidthMm: w1, | |
| sheetHeightMm: h1, | |
| cutsFromParent: 1, | |
| cutScheme: buildFullCutScheme(parentW, parentH), | |
| gripperMarginMm, | |
| }); | |
| } | |
| // Phương án 1: Chia 2 (Cắt đôi) | |
| // Cắt đôi chiều dài (thường tạo ra khổ 430 x 650 mm từ 65x86) | |
| const longSide = Math.max(parentW, parentH); | |
| const shortSide = Math.min(parentW, parentH); | |
| const cut2_h = Math.floor(longSide / 2); | |
| const cut2_w = shortSide; | |
| candidates.push({ | |
| name: `Khổ chia 2 (${cut2_w} x ${cut2_h} mm)`, | |
| sheetWidthMm: Math.max(cut2_w, cut2_h), | |
| sheetHeightMm: Math.min(cut2_w, cut2_h), | |
| cutsFromParent: 2, | |
| cutScheme: buildCut2Scheme(parentW, parentH), | |
| gripperMarginMm, | |
| }); | |
| // Phương án 2: Chia 4 (Cắt chữ thập) | |
| const cut4_w = Math.floor(parentW / 2); | |
| const cut4_h = Math.floor(parentH / 2); | |
| candidates.push({ | |
| name: `Khổ chia 4 (${cut4_w} x ${cut4_h} mm)`, | |
| sheetWidthMm: Math.max(cut4_w, cut4_h), | |
| sheetHeightMm: Math.min(cut4_w, cut4_h), | |
| cutsFromParent: 4, | |
| cutScheme: buildCut4Scheme(parentW, parentH), | |
| gripperMarginMm, | |
| }); | |
| // Phương án 3: Chia 8 (Cắt 8 - cho ấn phẩm nhỏ như namecard, tag...) | |
| const cut8_1 = Math.floor(shortSide / 2); | |
| const cut8_2 = Math.floor(longSide / 4); | |
| candidates.push({ | |
| name: `Khổ chia 8 (${cut8_1} x ${cut8_2} mm)`, | |
| sheetWidthMm: Math.max(cut8_1, cut8_2), | |
| sheetHeightMm: Math.min(cut8_1, cut8_2), | |
| cutsFromParent: 8, | |
| cutScheme: buildCut8Scheme(parentW, parentH), | |
| gripperMarginMm, | |
| }); | |
| } | |
| // 2. Đánh giá tất cả phương án để tìm phương án tối ưu nhất | |
| let bestCandidateResult: { | |
| candidate: EvaluationCandidate; | |
| ups: number; | |
| cols: number; | |
| rows: number; | |
| isRotated: boolean; | |
| boxes: BoxCoordinate[]; | |
| efficiency: number; | |
| usableW: number; | |
| usableH: number; | |
| } | null = null; | |
| for (const cand of candidates) { | |
| const layout = evaluateImpositionLayout({ | |
| sheetW: cand.sheetWidthMm, | |
| sheetH: cand.sheetHeightMm, | |
| itemW: widthMm, | |
| itemH: heightMm, | |
| bleedMm, | |
| gripperMarginMm: cand.gripperMarginMm, | |
| isDigital: printTech === 'digital', | |
| }); | |
| if (printTech === 'digital') { | |
| if (digitalMode === 'with_paper') { | |
| // In nhanh kèm giấy: Chọn khổ phôi có số con (ups) trên tờ in lớn nhất | |
| // Nếu số con bằng nhau, ưu tiên 325x430 mm (hoặc 325x355 mm nếu là Fort F180/F230 vì rẻ hơn) | |
| if (!bestCandidateResult) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } else { | |
| if (layout.ups > bestCandidateResult.ups) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } else if (layout.ups === bestCandidateResult.ups && layout.ups > 0) { | |
| const isFortSpecial = paperType.code.toUpperCase().includes('F180') || paperType.code.toUpperCase().includes('F230'); | |
| const isCand355 = cand.name.includes('355'); | |
| if (isFortSpecial && isCand355) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } | |
| } | |
| } | |
| } else { | |
| // In gia công Konica C12000: | |
| // Chọn khổ máy in có layout.ups > 0 và chi phí ước tính trên mỗi con thành phẩm là thấp nhất | |
| // (Tránh trường hợp in tờ rơi A4 mà lại chọn nhầm khổ banner 1.2 mét) | |
| const approxClick = getApproxKonicaClick(cand.sheetWidthMm, cand.sheetHeightMm); | |
| const costPerPiece = layout.ups > 0 ? approxClick / layout.ups : Infinity; | |
| if (!bestCandidateResult) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } else { | |
| const prevClick = getApproxKonicaClick(bestCandidateResult.candidate.sheetWidthMm, bestCandidateResult.candidate.sheetHeightMm); | |
| const prevCostPerPiece = bestCandidateResult.ups > 0 ? prevClick / bestCandidateResult.ups : Infinity; | |
| if (layout.ups > 0 && costPerPiece < prevCostPerPiece) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } | |
| } | |
| } | |
| } else { | |
| // In Offset: Ưu tiên tổng số con sinh ra từ 1 tờ giấy mẹ | |
| const totalUps = layout.ups * cand.cutsFromParent; | |
| if (!bestCandidateResult) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } else { | |
| const currentBestTotal = bestCandidateResult.ups * bestCandidateResult.candidate.cutsFromParent; | |
| if (totalUps > currentBestTotal) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } else if (totalUps === currentBestTotal) { | |
| if (cand.cutsFromParent > bestCandidateResult.candidate.cutsFromParent) { | |
| bestCandidateResult = { candidate: cand, ...layout }; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Fallback nếu không có con nào vừa | |
| if (!bestCandidateResult || bestCandidateResult.ups === 0) { | |
| const fallbackCand = candidates[0]; | |
| bestCandidateResult = { | |
| candidate: fallbackCand, | |
| ups: 1, | |
| cols: 1, | |
| rows: 1, | |
| isRotated: false, | |
| boxes: [ | |
| { | |
| index: 1, | |
| x: 10, | |
| y: 10, | |
| w: widthMm + bleedMm * 2, | |
| h: heightMm + bleedMm * 2, | |
| trimW: widthMm, | |
| trimH: heightMm, | |
| bleedMm, | |
| isRotated: false, | |
| }, | |
| ], | |
| efficiency: 10, | |
| usableW: fallbackCand.sheetWidthMm, | |
| usableH: fallbackCand.sheetHeightMm, | |
| }; | |
| } | |
| const { candidate, ups, cols, rows, isRotated, boxes, efficiency, usableW, usableH } = bestCandidateResult; | |
| const isWithPaper = printTech === 'digital' && digitalMode === 'with_paper'; | |
| return { | |
| parentSheet: { | |
| widthCm: isWithPaper ? candidate.sheetWidthMm / 10 : paperType.parentWidthCm, | |
| heightCm: isWithPaper ? candidate.sheetHeightMm / 10 : paperType.parentHeightCm, | |
| name: isWithPaper | |
| ? `Phôi giấy INTC (${candidate.sheetWidthMm / 10}x${candidate.sheetHeightMm / 10}cm)` | |
| : `${paperType.name} (${paperType.parentWidthCm}x${paperType.parentHeightCm}cm)`, | |
| }, | |
| printSheet: { | |
| widthMm: candidate.sheetWidthMm, | |
| heightMm: candidate.sheetHeightMm, | |
| name: candidate.name, | |
| }, | |
| upsPerPrintSheet: ups, | |
| cutsPerParentSheet: isWithPaper ? 1 : candidate.cutsFromParent, | |
| totalUpsPerParentSheet: isWithPaper ? ups : ups * candidate.cutsFromParent, | |
| gripperMarginMm: candidate.gripperMarginMm, | |
| usableWidthMm: usableW, | |
| usableHeightMm: usableH, | |
| cols, | |
| rows, | |
| isRotated, | |
| boxes, | |
| sheetEfficiencyPercent: Math.round(efficiency * 10) / 10, | |
| cutScheme: candidate.cutScheme, | |
| }; | |
| } | |
| function getApproxKonicaClick(w: number, h: number): number { | |
| const maxDim = Math.max(w, h); | |
| if (maxDim <= 300) return 800; // A4 | |
| if (maxDim <= 360) return 1000; // 330x355 | |
| if (maxDim <= 425) return 1600; // A3 | |
| if (maxDim <= 500) return 2500; // 330x483 (A3+) | |
| return 7000; // 330x1200 Banner | |
| } | |
| // ----------------------------------------------------------------------------------------- | |
| // THUẬT TOÁN XẾP BÌNH BÀI CHUẨN XÉNG CÔNG NGHIỆP TRÊN TỜ IN MÁY | |
| // ----------------------------------------------------------------------------------------- | |
| interface LayoutEvalParams { | |
| sheetW: number; | |
| sheetH: number; | |
| itemW: number; | |
| itemH: number; | |
| bleedMm: number; | |
| gripperMarginMm: number; | |
| isDigital?: boolean; | |
| } | |
| interface LayoutResult { | |
| ups: number; | |
| cols: number; | |
| rows: number; | |
| isRotated: boolean; | |
| boxes: BoxCoordinate[]; | |
| efficiency: number; | |
| usableW: number; | |
| usableH: number; | |
| } | |
| function evaluateImpositionLayout({ | |
| sheetW, | |
| sheetH, | |
| itemW, | |
| itemH, | |
| bleedMm, | |
| gripperMarginMm, | |
| isDigital = false, | |
| }: LayoutEvalParams): LayoutResult { | |
| // Quy ước máy in: | |
| // Cạnh dài: width = max(sheetW, sheetH), Cạnh ngắn: height = min(sheetW, sheetH) | |
| const W = Math.max(sheetW, sheetH); | |
| const H = Math.min(sheetW, sheetH); | |
| let marginSide = 4; | |
| let marginTail = 4; | |
| let marginGripper = gripperMarginMm; | |
| if (isDigital) { | |
| // Máy in kỹ thuật số: Không có càng kẹp nhíp cơ học 10mm như offset. | |
| // Lề trắng máy in (non-printable margin) chỉ 2.5mm xung quanh 4 mép. | |
| marginSide = 2.5; | |
| marginTail = 2.5; | |
| marginGripper = 2.5; | |
| } | |
| const usableW = W - marginSide * 2; | |
| const usableH = H - (marginGripper + marginTail); | |
| if (usableW < itemW && usableW < itemH) { | |
| return { ups: 0, cols: 0, rows: 0, isRotated: false, boxes: [], efficiency: 0, usableW, usableH }; | |
| } | |
| // --- Cách 1: Xếp Thẳng (Không xoay) --- | |
| let cols1 = 0; | |
| let rows1 = 0; | |
| if (isDigital) { | |
| // In nhanh: Các con xếp sát nhau (cắt 1 nhát chung dao). Tràn lề ăn vào biên giấy ngoài cùng. | |
| cols1 = Math.max(0, Math.floor(usableW / itemW)); | |
| rows1 = Math.max(0, Math.floor(usableH / itemH)); | |
| } else { | |
| // In offset: Chừa lề kẹp nhíp và đường tràn lề | |
| cols1 = Math.max(0, Math.floor((usableW - 2 * bleedMm) / itemW)); | |
| rows1 = Math.max(0, Math.floor((usableH - 2 * bleedMm) / itemH)); | |
| } | |
| const ups1 = cols1 * rows1; | |
| // --- Cách 2: Xếp Xoay 90 độ --- | |
| let cols2 = 0; | |
| let rows2 = 0; | |
| if (isDigital) { | |
| cols2 = Math.max(0, Math.floor(usableW / itemH)); | |
| rows2 = Math.max(0, Math.floor(usableH / itemW)); | |
| } else { | |
| cols2 = Math.max(0, Math.floor((usableW - 2 * bleedMm) / itemH)); | |
| rows2 = Math.max(0, Math.floor((usableH - 2 * bleedMm) / itemW)); | |
| } | |
| const ups2 = cols2 * rows2; | |
| // Chọn phương án tốt nhất giữa Xếp Thẳng (ups1) và Xếp Xoay 90° (ups2) | |
| let bestUps = ups1; | |
| let bestCols = cols1; | |
| let bestRows = rows1; | |
| let isRotated = false; | |
| if (ups2 > bestUps) { | |
| bestUps = ups2; | |
| bestCols = cols2; | |
| bestRows = rows2; | |
| isRotated = true; | |
| } | |
| // Tạo danh sách tọa độ các hộp sản phẩm | |
| const boxes: BoxCoordinate[] = []; | |
| const boxW = isRotated ? itemH : itemW; | |
| const boxH = isRotated ? itemW : itemH; | |
| // Canh giữa cụm con trong tờ in | |
| const blockW = bestCols * boxW; | |
| const blockH = bestRows * boxH; | |
| const startX = isDigital | |
| ? Math.max(0, Math.round(((W - blockW) / 2) * 10) / 10) | |
| : marginSide + Math.max(0, Math.floor((usableW - (blockW + 2 * bleedMm)) / 2)) + bleedMm; | |
| const startY = isDigital | |
| ? Math.max(0, Math.round(((H - blockH) / 2) * 10) / 10) | |
| : marginTail + Math.max(0, Math.floor((usableH - (blockH + 2 * bleedMm)) / 2)) + bleedMm; | |
| let idx = 1; | |
| for (let r = 0; r < bestRows; r++) { | |
| for (let c = 0; c < bestCols; c++) { | |
| boxes.push({ | |
| index: idx++, | |
| x: Math.round((startX + c * boxW) * 10) / 10, | |
| y: Math.round((startY + r * boxH) * 10) / 10, | |
| w: boxW, | |
| h: boxH, | |
| trimW: itemW, | |
| trimH: itemH, | |
| bleedMm, | |
| isRotated, | |
| }); | |
| } | |
| } | |
| const totalItemArea = itemW * itemH * bestUps; | |
| const sheetArea = W * H; | |
| const efficiency = sheetArea > 0 ? (totalItemArea / sheetArea) * 100 : 0; | |
| return { | |
| ups: bestUps, | |
| cols: bestCols, | |
| rows: bestRows, | |
| isRotated, | |
| boxes, | |
| efficiency, | |
| usableW, | |
| usableH, | |
| }; | |
| } | |
| // ----------------------------------------------------------------------------------------- | |
| // SINH SƠ ĐỒ CẮT GIẤY MẸ TRỰC QUAN (PAPER CUTTING SCHEME) | |
| // ----------------------------------------------------------------------------------------- | |
| export function determineCutScheme( | |
| parentW: number, | |
| parentH: number, | |
| printW: number, | |
| printH: number, | |
| cuts: number | |
| ): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); | |
| const ph = Math.min(parentW, parentH); | |
| if (cuts <= 1) return buildFullCutScheme(pw, ph); | |
| if (cuts === 2) return buildCut2Scheme(pw, ph); | |
| if (cuts === 4) return buildCut4Scheme(pw, ph); | |
| if (cuts === 8) return buildCut8Scheme(pw, ph); | |
| return buildSimpleCutScheme(pw, ph, printW, printH, cuts); | |
| } | |
| function buildFullCutScheme(parentW: number, parentH: number): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); | |
| const ph = Math.min(parentW, parentH); | |
| return { | |
| parentWidthMm: pw, | |
| parentHeightMm: ph, | |
| cutType: 'chia_1', | |
| cutDescription: `In nguyên khổ ${pw} x ${ph} mm (không cần xén trước khi in)`, | |
| cutsCount: 1, | |
| cutSheetWidthMm: pw, | |
| cutSheetHeightMm: ph, | |
| wasteAreaPercent: 0, | |
| cutLines: [], | |
| blocks: [ | |
| { | |
| index: 1, | |
| x: 0, | |
| y: 0, | |
| w: pw, | |
| h: ph, | |
| name: `Tờ in #1 (${pw} x ${ph} mm)`, | |
| }, | |
| ], | |
| }; | |
| } | |
| function buildCut2Scheme(parentW: number, parentH: number): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); // ví dụ 860mm | |
| const ph = Math.min(parentW, parentH); // ví dụ 650mm | |
| const cutW = Math.floor(pw / 2); // 430mm | |
| return { | |
| parentWidthMm: pw, | |
| parentHeightMm: ph, | |
| cutType: 'chia_2', | |
| cutDescription: `Xén đôi chiều ${pw}mm thành 2 tờ ${ph} x ${cutW} mm (Chia 2)`, | |
| cutsCount: 2, | |
| cutSheetWidthMm: ph, | |
| cutSheetHeightMm: cutW, | |
| wasteAreaPercent: 0, | |
| cutLines: [ | |
| { | |
| x1: cutW, | |
| y1: 0, | |
| x2: cutW, | |
| y2: ph, | |
| type: 'vertical', | |
| label: `Xén dọc: ${cutW}mm`, | |
| }, | |
| ], | |
| blocks: [ | |
| { | |
| index: 1, | |
| x: 0, | |
| y: 0, | |
| w: cutW, | |
| h: ph, | |
| name: `Tờ in #1 (${ph} x ${cutW} mm)`, | |
| }, | |
| { | |
| index: 2, | |
| x: cutW, | |
| y: 0, | |
| w: pw - cutW, | |
| h: ph, | |
| name: `Tờ in #2 (${ph} x ${cutW} mm)`, | |
| }, | |
| ], | |
| }; | |
| } | |
| function buildCut4Scheme(parentW: number, parentH: number): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); // ví dụ 860 hoặc 1090 | |
| const ph = Math.min(parentW, parentH); // ví dụ 650 hoặc 790 | |
| const halfW = Math.floor(pw / 2); // 430 | |
| const halfH = Math.floor(ph / 2); // 325 | |
| return { | |
| parentWidthMm: pw, | |
| parentHeightMm: ph, | |
| cutType: 'chia_4', | |
| cutDescription: `Xén chữ thập (Chia 4) thành 4 tờ ${halfW} x ${halfH} mm`, | |
| cutsCount: 4, | |
| cutSheetWidthMm: halfW, | |
| cutSheetHeightMm: halfH, | |
| wasteAreaPercent: 0, | |
| cutLines: [ | |
| { | |
| x1: halfW, | |
| y1: 0, | |
| x2: halfW, | |
| y2: ph, | |
| type: 'vertical', | |
| label: `Xén dọc: ${halfW}mm`, | |
| }, | |
| { | |
| x1: 0, | |
| y1: halfH, | |
| x2: pw, | |
| y2: halfH, | |
| type: 'horizontal', | |
| label: `Xén ngang: ${halfH}mm`, | |
| }, | |
| ], | |
| blocks: [ | |
| { index: 1, x: 0, y: 0, w: halfW, h: halfH, name: `Tờ in #1 (${halfW} x ${halfH} mm)` }, | |
| { index: 2, x: halfW, y: 0, w: pw - halfW, h: halfH, name: `Tờ in #2 (${halfW} x ${halfH} mm)` }, | |
| { index: 3, x: 0, y: halfH, w: halfW, h: ph - halfH, name: `Tờ in #3 (${halfW} x ${halfH} mm)` }, | |
| { index: 4, x: halfW, y: halfH, w: pw - halfW, h: ph - halfH, name: `Tờ in #4 (${halfW} x ${halfH} mm)` }, | |
| ], | |
| }; | |
| } | |
| function buildCut8Scheme(parentW: number, parentH: number): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); // ví dụ 860 | |
| const ph = Math.min(parentW, parentH); // ví dụ 650 | |
| const colW = Math.floor(pw / 4); // 215 | |
| const rowH = Math.floor(ph / 2); // 325 | |
| const blocks: PaperCutBlock[] = []; | |
| const cutLines: PaperCutLine[] = [ | |
| { x1: colW, y1: 0, x2: colW, y2: ph, type: 'vertical', label: `Xén: ${colW}mm` }, | |
| { x1: colW * 2, y1: 0, x2: colW * 2, y2: ph, type: 'vertical', label: `Xén: ${colW * 2}mm` }, | |
| { x1: colW * 3, y1: 0, x2: colW * 3, y2: ph, type: 'vertical', label: `Xén: ${colW * 3}mm` }, | |
| { x1: 0, y1: rowH, x2: pw, y2: rowH, type: 'horizontal', label: `Xén ngang: ${rowH}mm` }, | |
| ]; | |
| let idx = 1; | |
| for (let r = 0; r < 2; r++) { | |
| for (let c = 0; c < 4; c++) { | |
| const curW = c === 3 ? pw - colW * 3 : colW; | |
| const curH = r === 1 ? ph - rowH : rowH; | |
| blocks.push({ | |
| index: idx++, | |
| x: c * colW, | |
| y: r * rowH, | |
| w: curW, | |
| h: curH, | |
| name: `Tờ in #${idx - 1} (${rowH} x ${colW} mm)`, | |
| }); | |
| } | |
| } | |
| return { | |
| parentWidthMm: pw, | |
| parentHeightMm: ph, | |
| cutType: 'chia_8', | |
| cutDescription: `Xén chia 8 thành 8 tờ ${rowH} x ${colW} mm`, | |
| cutsCount: 8, | |
| cutSheetWidthMm: rowH, | |
| cutSheetHeightMm: colW, | |
| wasteAreaPercent: 0, | |
| cutLines, | |
| blocks, | |
| }; | |
| } | |
| function buildSimpleCutScheme( | |
| parentW: number, | |
| parentH: number, | |
| printW: number, | |
| printH: number, | |
| cuts: number | |
| ): PaperCutScheme { | |
| const pw = Math.max(parentW, parentH); | |
| const ph = Math.min(parentW, parentH); | |
| // So sánh 2 chiều xếp trên tờ mẹ | |
| const c1 = Math.floor(pw / printW); | |
| const r1 = Math.floor(ph / printH); | |
| const tot1 = c1 * r1; | |
| const c2 = Math.floor(pw / printH); | |
| const r2 = Math.floor(ph / printW); | |
| const tot2 = c2 * r2; | |
| const useRotated = tot2 > tot1; | |
| const pieceW = useRotated ? printH : printW; | |
| const pieceH = useRotated ? printW : printH; | |
| const cols = useRotated ? c2 : c1; | |
| const rows = useRotated ? r2 : r1; | |
| const actualCols = Math.max(1, cols); | |
| const actualRows = Math.max(1, rows); | |
| const blocks: PaperCutBlock[] = []; | |
| const cutLines: PaperCutLine[] = []; | |
| let idx = 1; | |
| for (let r = 0; r < actualRows; r++) { | |
| for (let c = 0; c < actualCols; c++) { | |
| if (idx <= cuts) { | |
| blocks.push({ | |
| index: idx++, | |
| x: c * pieceW, | |
| y: r * pieceH, | |
| w: pieceW, | |
| h: pieceH, | |
| name: `Tờ in #${idx - 1} (${pieceW} x ${pieceH} mm)`, | |
| }); | |
| } | |
| } | |
| } | |
| // Tạo cut lines | |
| for (let c = 1; c < actualCols; c++) { | |
| cutLines.push({ | |
| x1: c * pieceW, | |
| y1: 0, | |
| x2: c * pieceW, | |
| y2: Math.min(ph, actualRows * pieceH), | |
| type: 'vertical', | |
| label: `Xén: ${c * pieceW}mm`, | |
| }); | |
| } | |
| for (let r = 1; r < actualRows; r++) { | |
| cutLines.push({ | |
| x1: 0, | |
| y1: r * pieceH, | |
| x2: Math.min(pw, actualCols * pieceW), | |
| y2: r * pieceH, | |
| type: 'horizontal', | |
| label: `Xén: ${r * pieceH}mm`, | |
| }); | |
| } | |
| const usedArea = cuts * pieceW * pieceH; | |
| const totalArea = pw * ph; | |
| const wastePercent = totalArea > 0 ? Math.max(0, Math.round(((totalArea - usedArea) / totalArea) * 100)) : 0; | |
| return { | |
| parentWidthMm: pw, | |
| parentHeightMm: ph, | |
| cutType: cuts === 2 ? 'chia_2' : cuts === 4 ? 'chia_4' : cuts === 8 ? 'chia_8' : 'custom', | |
| cutDescription: `Cắt ${cuts} tờ in (${pieceW} x ${pieceH} mm) từ tờ giấy mẹ ${pw} x ${ph} mm`, | |
| cutsCount: cuts, | |
| cutSheetWidthMm: pieceW, | |
| cutSheetHeightMm: pieceH, | |
| wasteAreaPercent: wastePercent, | |
| cutLines, | |
| blocks, | |
| }; | |
| } | |
| function calculateCutsFromParent(parentW: number, parentH: number, printW: number, printH: number): number { | |
| const cuts1 = Math.floor(parentW / printW) * Math.floor(parentH / printH); | |
| const cuts2 = Math.floor(parentW / printH) * Math.floor(parentH / printW); | |
| return Math.max(cuts1, cuts2, 1); | |
| } | |