fix(imposition): correct imposition calculation, dual-tab print and paper cutting diagrams
Browse files- src/app/page.tsx +1 -1
- src/components/ImpositionVisualizer.tsx +317 -63
- src/lib/engine/imposition.ts +492 -149
- src/types/index.ts +36 -0
src/app/page.tsx
CHANGED
|
@@ -724,7 +724,7 @@ export default function HomePage() {
|
|
| 724 |
}`}
|
| 725 |
>
|
| 726 |
<LayoutGrid className="w-3.5 h-3.5" />
|
| 727 |
-
Sơ Đồ Cắt Giấy
|
| 728 |
</button>
|
| 729 |
|
| 730 |
<button
|
|
|
|
| 724 |
}`}
|
| 725 |
>
|
| 726 |
<LayoutGrid className="w-3.5 h-3.5" />
|
| 727 |
+
Sơ Đồ Bình Bài & Cắt Giấy
|
| 728 |
</button>
|
| 729 |
|
| 730 |
<button
|
src/components/ImpositionVisualizer.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import React from 'react';
|
| 4 |
import { ImpositionResult } from '@/types';
|
| 5 |
-
import { Layers,
|
| 6 |
|
| 7 |
interface ImpositionVisualizerProps {
|
| 8 |
imposition: ImpositionResult;
|
|
@@ -16,92 +16,340 @@ export const ImpositionVisualizer: React.FC<ImpositionVisualizerProps> = ({
|
|
| 16 |
productWidthMm,
|
| 17 |
productHeightMm,
|
| 18 |
}) => {
|
|
|
|
|
|
|
| 19 |
const {
|
| 20 |
printSheet,
|
| 21 |
parentSheet,
|
| 22 |
upsPerPrintSheet,
|
| 23 |
cutsPerParentSheet,
|
|
|
|
| 24 |
gripperMarginMm,
|
| 25 |
cols,
|
| 26 |
rows,
|
| 27 |
boxes,
|
| 28 |
sheetEfficiencyPercent,
|
| 29 |
isRotated,
|
|
|
|
| 30 |
} = imposition;
|
| 31 |
|
| 32 |
-
|
| 33 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
return (
|
| 36 |
<div className="space-y-3">
|
| 37 |
-
{/*
|
| 38 |
-
<div className="
|
| 39 |
-
<
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
</
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
fill="url(#gripperStripe)"
|
| 62 |
-
opacity="0.8"
|
| 63 |
-
/>
|
| 64 |
-
<text
|
| 65 |
-
x={svgWidth / 2}
|
| 66 |
-
y={svgHeight - gripperMarginMm / 2 + 3}
|
| 67 |
-
textAnchor="middle"
|
| 68 |
-
fill="#92400e"
|
| 69 |
-
fontSize={Math.max(9, Math.min(14, svgWidth / 35))}
|
| 70 |
-
fontWeight="bold"
|
| 71 |
>
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
<rect
|
| 79 |
-
x=
|
| 80 |
-
y={
|
| 81 |
-
width={
|
| 82 |
-
height={
|
| 83 |
-
fill="#
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
strokeWidth="1"
|
| 86 |
-
rx="1"
|
| 87 |
/>
|
| 88 |
<text
|
| 89 |
-
x={
|
| 90 |
-
y={
|
| 91 |
textAnchor="middle"
|
| 92 |
-
fill="#
|
| 93 |
-
fontSize={Math.max(9, Math.min(13,
|
| 94 |
fontWeight="bold"
|
| 95 |
>
|
| 96 |
-
|
| 97 |
</text>
|
| 98 |
-
</g>
|
| 99 |
-
))}
|
| 100 |
-
</svg>
|
| 101 |
-
</div>
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
| 106 |
<span className="text-[11px] text-slate-500 block">Số con / tờ in</span>
|
| 107 |
<span className="text-sm font-bold text-blue-700">{upsPerPrintSheet} con</span>
|
|
@@ -109,15 +357,21 @@ export const ImpositionVisualizer: React.FC<ImpositionVisualizerProps> = ({
|
|
| 109 |
</div>
|
| 110 |
|
| 111 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
| 112 |
-
<span className="text-[11px] text-slate-500 block">Cắt
|
| 113 |
<span className="text-sm font-bold text-indigo-700">Chia {cutsPerParentSheet}</span>
|
| 114 |
<span className="text-[10px] text-slate-400 block">{parentSheet.widthCm}×{parentSheet.heightCm}cm</span>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
<span className="text-[11px] text-slate-500 block">Tận dụng giấy</span>
|
| 119 |
<span className="text-sm font-bold text-emerald-700">{sheetEfficiencyPercent}%</span>
|
| 120 |
-
<span className="text-[10px] text-slate-400 block">{
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
</div>
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import React, { useState } from 'react';
|
| 4 |
import { ImpositionResult } from '@/types';
|
| 5 |
+
import { Layers, Scissors, Printer, CheckCircle2, RotateCw } from 'lucide-react';
|
| 6 |
|
| 7 |
interface ImpositionVisualizerProps {
|
| 8 |
imposition: ImpositionResult;
|
|
|
|
| 16 |
productWidthMm,
|
| 17 |
productHeightMm,
|
| 18 |
}) => {
|
| 19 |
+
const [activeTab, setActiveTab] = useState<'imposition' | 'paperCut'>('imposition');
|
| 20 |
+
|
| 21 |
const {
|
| 22 |
printSheet,
|
| 23 |
parentSheet,
|
| 24 |
upsPerPrintSheet,
|
| 25 |
cutsPerParentSheet,
|
| 26 |
+
totalUpsPerParentSheet,
|
| 27 |
gripperMarginMm,
|
| 28 |
cols,
|
| 29 |
rows,
|
| 30 |
boxes,
|
| 31 |
sheetEfficiencyPercent,
|
| 32 |
isRotated,
|
| 33 |
+
cutScheme,
|
| 34 |
} = imposition;
|
| 35 |
|
| 36 |
+
// Kích thước SVG tờ in máy
|
| 37 |
+
const printW = printSheet.widthMm;
|
| 38 |
+
const printH = printSheet.heightMm;
|
| 39 |
+
|
| 40 |
+
// Kích thước SVG tờ giấy mẹ
|
| 41 |
+
const parentW = cutScheme?.parentWidthMm || parentSheet.widthCm * 10;
|
| 42 |
+
const parentH = cutScheme?.parentHeightMm || parentSheet.heightCm * 10;
|
| 43 |
+
|
| 44 |
+
// Bảng màu cho các tờ in khi xén từ tờ mẹ
|
| 45 |
+
const blockColors = [
|
| 46 |
+
{ bg: '#eff6ff', stroke: '#3b82f6', text: '#1d4ed8' }, // blue
|
| 47 |
+
{ bg: '#f0fdf4', stroke: '#22c55e', text: '#15803d' }, // green
|
| 48 |
+
{ bg: '#fefce8', stroke: '#eab308', text: '#a16207' }, // yellow
|
| 49 |
+
{ bg: '#faf5ff', stroke: '#a855f7', text: '#7e22ce' }, // purple
|
| 50 |
+
{ bg: '#fff7ed', stroke: '#f97316', text: '#c2410c' }, // orange
|
| 51 |
+
{ bg: '#f0fdfa', stroke: '#14b8a6', text: '#0f766e' }, // teal
|
| 52 |
+
{ bg: '#fff1f2', stroke: '#f43f5e', text: '#be123c' }, // rose
|
| 53 |
+
{ bg: '#eef2ff', stroke: '#6366f1', text: '#4338ca' }, // indigo
|
| 54 |
+
];
|
| 55 |
|
| 56 |
return (
|
| 57 |
<div className="space-y-3">
|
| 58 |
+
{/* Sub-tab chuyển đổi giữa Sơ đồ bình bài và Sơ đồ cắt giấy mẹ */}
|
| 59 |
+
<div className="flex items-center justify-between gap-2 border-b border-slate-200 pb-2">
|
| 60 |
+
<div className="flex items-center gap-1.5 bg-slate-100 p-1 rounded-lg text-xs font-medium">
|
| 61 |
+
<button
|
| 62 |
+
type="button"
|
| 63 |
+
onClick={() => setActiveTab('imposition')}
|
| 64 |
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-all ${
|
| 65 |
+
activeTab === 'imposition'
|
| 66 |
+
? 'bg-white text-blue-700 font-semibold shadow-2xs'
|
| 67 |
+
: 'text-slate-600 hover:text-slate-900'
|
| 68 |
+
}`}
|
| 69 |
+
>
|
| 70 |
+
<Printer className="w-3.5 h-3.5" />
|
| 71 |
+
Sơ Đồ Bình Bài (Tờ In Máy)
|
| 72 |
+
</button>
|
| 73 |
+
|
| 74 |
+
<button
|
| 75 |
+
type="button"
|
| 76 |
+
onClick={() => setActiveTab('paperCut')}
|
| 77 |
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-all ${
|
| 78 |
+
activeTab === 'paperCut'
|
| 79 |
+
? 'bg-white text-amber-700 font-semibold shadow-2xs'
|
| 80 |
+
: 'text-slate-600 hover:text-slate-900'
|
| 81 |
+
}`}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
>
|
| 83 |
+
<Scissors className="w-3.5 h-3.5" />
|
| 84 |
+
Sơ Đồ Cắt Giấy Mẹ ({cutsPerParentSheet > 1 ? `Chia ${cutsPerParentSheet}` : 'Nguyên Khổ'})
|
| 85 |
+
</button>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<span className="text-[11px] font-medium text-slate-500 hidden sm:inline-block">
|
| 89 |
+
{activeTab === 'imposition'
|
| 90 |
+
? `Khổ in: ${printW}×${printH}mm`
|
| 91 |
+
: `Khổ mẹ: ${parentW}×${parentH}mm`}
|
| 92 |
+
</span>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
{/* VÙNG 1: SƠ ĐỒ BÌNH BÀI TRÊN TỜ IN MÁY */}
|
| 96 |
+
{activeTab === 'imposition' && (
|
| 97 |
+
<div className="space-y-2">
|
| 98 |
+
<div className="relative bg-slate-900/95 rounded-xl p-3 flex flex-col items-center justify-center min-h-[220px] max-h-[270px] overflow-hidden border border-slate-800 shadow-inner">
|
| 99 |
+
<svg
|
| 100 |
+
viewBox={`0 0 ${printW} ${printH}`}
|
| 101 |
+
className="w-full max-h-[230px] drop-shadow-xl border border-slate-700 rounded bg-white transition-all duration-300 select-none"
|
| 102 |
+
style={{ aspectRatio: `${printW} / ${printH}` }}
|
| 103 |
+
>
|
| 104 |
+
<defs>
|
| 105 |
+
{/* Hoa văn sọc vàng cho lề kẹp nhíp */}
|
| 106 |
+
<pattern
|
| 107 |
+
id="gripperStripe"
|
| 108 |
+
width="10"
|
| 109 |
+
height="10"
|
| 110 |
+
patternUnits="userSpaceOnUse"
|
| 111 |
+
patternTransform="rotate(45)"
|
| 112 |
+
>
|
| 113 |
+
<line x1="0" y1="0" x2="0" y2="10" stroke="#fde047" strokeWidth="3" />
|
| 114 |
+
<line x1="5" y1="0" x2="5" y2="10" stroke="#eab308" strokeWidth="3" />
|
| 115 |
+
</pattern>
|
| 116 |
+
</defs>
|
| 117 |
|
| 118 |
+
{/* Tờ in nền trắng */}
|
| 119 |
+
<rect x="0" y="0" width={printW} height={printH} fill="#ffffff" />
|
| 120 |
+
|
| 121 |
+
{/* Đường biên vùng in hữu dụng (vùng an toàn) */}
|
| 122 |
+
<rect
|
| 123 |
+
x="4"
|
| 124 |
+
y="4"
|
| 125 |
+
width={printW - 8}
|
| 126 |
+
height={printH - (gripperMarginMm + 4)}
|
| 127 |
+
fill="none"
|
| 128 |
+
stroke="#cbd5e1"
|
| 129 |
+
strokeWidth="0.8"
|
| 130 |
+
strokeDasharray="4 3"
|
| 131 |
+
/>
|
| 132 |
+
|
| 133 |
+
{/* Dải lề kẹp nhíp (Gripper margin) ở cạnh đáy */}
|
| 134 |
<rect
|
| 135 |
+
x="0"
|
| 136 |
+
y={printH - gripperMarginMm}
|
| 137 |
+
width={printW}
|
| 138 |
+
height={gripperMarginMm}
|
| 139 |
+
fill="url(#gripperStripe)"
|
| 140 |
+
opacity="0.9"
|
| 141 |
+
/>
|
| 142 |
+
<line
|
| 143 |
+
x1="0"
|
| 144 |
+
y1={printH - gripperMarginMm}
|
| 145 |
+
x2={printW}
|
| 146 |
+
y2={printH - gripperMarginMm}
|
| 147 |
+
stroke="#ca8a04"
|
| 148 |
strokeWidth="1"
|
|
|
|
| 149 |
/>
|
| 150 |
<text
|
| 151 |
+
x={printW / 2}
|
| 152 |
+
y={printH - gripperMarginMm / 2 + 3}
|
| 153 |
textAnchor="middle"
|
| 154 |
+
fill="#854d0e"
|
| 155 |
+
fontSize={Math.max(9, Math.min(13, printW / 38))}
|
| 156 |
fontWeight="bold"
|
| 157 |
>
|
| 158 |
+
▼ Lề kẹp nhíp: {gripperMarginMm}mm ({printW >= 800 ? 'Bắt nhíp chiều 86cm' : `Bắt nhíp chiều ${Math.round(printW / 10)}cm`})
|
| 159 |
</text>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
+
{/* Danh sách các con sản phẩm (Ups) */}
|
| 162 |
+
{boxes.map((b) => {
|
| 163 |
+
const isVerySmall = b.w < 50 || b.h < 35;
|
| 164 |
+
const fontSize = Math.max(9, Math.min(13, b.w / 5));
|
| 165 |
+
|
| 166 |
+
return (
|
| 167 |
+
<g key={b.index}>
|
| 168 |
+
{/* Ô sản phẩm */}
|
| 169 |
+
<rect
|
| 170 |
+
x={b.x}
|
| 171 |
+
y={b.y}
|
| 172 |
+
width={b.w}
|
| 173 |
+
height={b.h}
|
| 174 |
+
fill="#eff6ff"
|
| 175 |
+
stroke="#2563eb"
|
| 176 |
+
strokeWidth="0.9"
|
| 177 |
+
/>
|
| 178 |
+
|
| 179 |
+
{/* Dấu xén (Crop marks) nhỏ ở góc nếu kích thước vừa */}
|
| 180 |
+
{!isVerySmall && (
|
| 181 |
+
<>
|
| 182 |
+
<line x1={b.x} y1={b.y + 4} x2={b.x} y2={b.y} stroke="#93c5fd" strokeWidth="0.8" />
|
| 183 |
+
<line x1={b.x} y1={b.y} x2={b.x + 4} y2={b.y} stroke="#93c5fd" strokeWidth="0.8" />
|
| 184 |
+
<line x1={b.x + b.w - 4} y1={b.y} x2={b.x + b.w} y2={b.y} stroke="#93c5fd" strokeWidth="0.8" />
|
| 185 |
+
<line x1={b.x + b.w} y1={b.y} x2={b.x + b.w} y2={b.y + 4} stroke="#93c5fd" strokeWidth="0.8" />
|
| 186 |
+
</>
|
| 187 |
+
)}
|
| 188 |
+
|
| 189 |
+
{/* Số thứ tự con */}
|
| 190 |
+
<text
|
| 191 |
+
x={b.x + b.w / 2}
|
| 192 |
+
y={isVerySmall ? b.y + b.h / 2 + 3 : b.y + b.h / 2 - 2}
|
| 193 |
+
textAnchor="middle"
|
| 194 |
+
fill="#1d4ed8"
|
| 195 |
+
fontSize={fontSize}
|
| 196 |
+
fontWeight="bold"
|
| 197 |
+
>
|
| 198 |
+
#{b.index}
|
| 199 |
+
</text>
|
| 200 |
+
|
| 201 |
+
{/* Kích thước con (nếu con đủ lớn) */}
|
| 202 |
+
{!isVerySmall && (
|
| 203 |
+
<text
|
| 204 |
+
x={b.x + b.w / 2}
|
| 205 |
+
y={b.y + b.h / 2 + fontSize * 0.9}
|
| 206 |
+
textAnchor="middle"
|
| 207 |
+
fill="#60a5fa"
|
| 208 |
+
fontSize={Math.max(7, fontSize * 0.75)}
|
| 209 |
+
>
|
| 210 |
+
{Math.round(b.w)}×{Math.round(b.h)}mm
|
| 211 |
+
</text>
|
| 212 |
+
)}
|
| 213 |
+
</g>
|
| 214 |
+
);
|
| 215 |
+
})}
|
| 216 |
+
</svg>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<div className="flex items-center justify-between text-[11px] text-slate-500 px-1">
|
| 220 |
+
<span className="flex items-center gap-1">
|
| 221 |
+
<span className="w-2.5 h-2.5 bg-blue-500 rounded-2xs inline-block" />
|
| 222 |
+
Sản phẩm thành phẩm ({productWidthMm}×{productHeightMm}mm)
|
| 223 |
+
{isRotated && (
|
| 224 |
+
<span className="text-amber-600 font-semibold inline-flex items-center gap-0.5 ml-1">
|
| 225 |
+
<RotateCw className="w-3 h-3" /> Xoay 90°
|
| 226 |
+
</span>
|
| 227 |
+
)}
|
| 228 |
+
</span>
|
| 229 |
+
<span className="flex items-center gap-1">
|
| 230 |
+
<span className="w-2.5 h-2.5 bg-amber-400 rounded-2xs inline-block" />
|
| 231 |
+
Lề kẹp nhíp ({gripperMarginMm}mm)
|
| 232 |
+
</span>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
)}
|
| 236 |
+
|
| 237 |
+
{/* VÙNG 2: SƠ ĐỒ CẮT XẢ GIẤY MẸ */}
|
| 238 |
+
{activeTab === 'paperCut' && (
|
| 239 |
+
<div className="space-y-2">
|
| 240 |
+
<div className="relative bg-slate-900/95 rounded-xl p-3 flex flex-col items-center justify-center min-h-[220px] max-h-[270px] overflow-hidden border border-slate-800 shadow-inner">
|
| 241 |
+
<svg
|
| 242 |
+
viewBox={`0 0 ${parentW} ${parentH}`}
|
| 243 |
+
className="w-full max-h-[230px] drop-shadow-xl border border-slate-700 rounded bg-white transition-all duration-300 select-none"
|
| 244 |
+
style={{ aspectRatio: `${parentW} / ${parentH}` }}
|
| 245 |
+
>
|
| 246 |
+
{/* Tờ giấy mẹ nền ngoài */}
|
| 247 |
+
<rect x="0" y="0" width={parentW} height={parentH} fill="#f1f5f9" stroke="#94a3b8" strokeWidth="1" />
|
| 248 |
+
|
| 249 |
+
{/* Các block tờ in con sau khi xén */}
|
| 250 |
+
{(cutScheme?.blocks || []).map((block, idx) => {
|
| 251 |
+
const color = blockColors[idx % blockColors.length];
|
| 252 |
+
const fontSize = Math.max(12, Math.min(22, block.w / 18));
|
| 253 |
+
|
| 254 |
+
return (
|
| 255 |
+
<g key={block.index}>
|
| 256 |
+
<rect
|
| 257 |
+
x={block.x}
|
| 258 |
+
y={block.y}
|
| 259 |
+
width={block.w}
|
| 260 |
+
height={block.h}
|
| 261 |
+
fill={color.bg}
|
| 262 |
+
stroke={color.stroke}
|
| 263 |
+
strokeWidth="1.2"
|
| 264 |
+
/>
|
| 265 |
+
|
| 266 |
+
{/* Tiêu đề tờ in con */}
|
| 267 |
+
<text
|
| 268 |
+
x={block.x + block.w / 2}
|
| 269 |
+
y={block.y + block.h / 2 - fontSize * 0.4}
|
| 270 |
+
textAnchor="middle"
|
| 271 |
+
fill={color.text}
|
| 272 |
+
fontSize={fontSize}
|
| 273 |
+
fontWeight="bold"
|
| 274 |
+
>
|
| 275 |
+
{block.name.split('(')[0].trim() || `Tờ in #${block.index}`}
|
| 276 |
+
</text>
|
| 277 |
+
|
| 278 |
+
{/* Kích thước khổ xén */}
|
| 279 |
+
<text
|
| 280 |
+
x={block.x + block.w / 2}
|
| 281 |
+
y={block.y + block.h / 2 + fontSize * 0.8}
|
| 282 |
+
textAnchor="middle"
|
| 283 |
+
fill="#475569"
|
| 284 |
+
fontSize={Math.max(10, fontSize * 0.75)}
|
| 285 |
+
fontWeight="medium"
|
| 286 |
+
>
|
| 287 |
+
{Math.round(block.w)} × {Math.round(block.h)} mm ({upsPerPrintSheet} con)
|
| 288 |
+
</text>
|
| 289 |
+
</g>
|
| 290 |
+
);
|
| 291 |
+
})}
|
| 292 |
+
|
| 293 |
+
{/* Các đường xén giấy (Cắt dao xén) màu đỏ nét đứt */}
|
| 294 |
+
{(cutScheme?.cutLines || []).map((line, idx) => {
|
| 295 |
+
const midX = (line.x1 + line.x2) / 2;
|
| 296 |
+
const midY = (line.y1 + line.y2) / 2;
|
| 297 |
+
|
| 298 |
+
return (
|
| 299 |
+
<g key={idx}>
|
| 300 |
+
<line
|
| 301 |
+
x1={line.x1}
|
| 302 |
+
y1={line.y1}
|
| 303 |
+
x2={line.x2}
|
| 304 |
+
y2={line.y2}
|
| 305 |
+
stroke="#dc2626"
|
| 306 |
+
strokeWidth="2"
|
| 307 |
+
strokeDasharray="6 4"
|
| 308 |
+
/>
|
| 309 |
+
|
| 310 |
+
{/* Biểu tượng cây kéo & nhãn đường xén */}
|
| 311 |
+
<g transform={`translate(${midX}, ${midY})`}>
|
| 312 |
+
<rect
|
| 313 |
+
x="-45"
|
| 314 |
+
y="-10"
|
| 315 |
+
width="90"
|
| 316 |
+
height="20"
|
| 317 |
+
rx="4"
|
| 318 |
+
fill="#fee2e2"
|
| 319 |
+
stroke="#ef4444"
|
| 320 |
+
strokeWidth="1"
|
| 321 |
+
/>
|
| 322 |
+
<text
|
| 323 |
+
x="0"
|
| 324 |
+
y="4"
|
| 325 |
+
textAnchor="middle"
|
| 326 |
+
fill="#991b1b"
|
| 327 |
+
fontSize="10"
|
| 328 |
+
fontWeight="bold"
|
| 329 |
+
>
|
| 330 |
+
✂ {line.label || 'Đường xén'}
|
| 331 |
+
</text>
|
| 332 |
+
</g>
|
| 333 |
+
</g>
|
| 334 |
+
);
|
| 335 |
+
})}
|
| 336 |
+
</svg>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<div className="bg-amber-50/80 border border-amber-200/80 rounded-lg p-2 flex items-center justify-between text-xs text-amber-900">
|
| 340 |
+
<span className="flex items-center gap-1.5 font-medium">
|
| 341 |
+
<Scissors className="w-3.5 h-3.5 text-amber-600 shrink-0" />
|
| 342 |
+
{cutScheme?.cutDescription || `Cắt xả từ tờ mẹ ${parentSheet.widthCm}×${parentSheet.heightCm}cm`}
|
| 343 |
+
</span>
|
| 344 |
+
<span className="font-semibold text-amber-800 shrink-0">
|
| 345 |
+
1 tờ mẹ = {cutsPerParentSheet} tờ in ({totalUpsPerParentSheet} con)
|
| 346 |
+
</span>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
)}
|
| 350 |
+
|
| 351 |
+
{/* THÔNG SỐ BÌNH TRANG VẮN TẮT */}
|
| 352 |
+
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 text-center text-xs">
|
| 353 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
| 354 |
<span className="text-[11px] text-slate-500 block">Số con / tờ in</span>
|
| 355 |
<span className="text-sm font-bold text-blue-700">{upsPerPrintSheet} con</span>
|
|
|
|
| 357 |
</div>
|
| 358 |
|
| 359 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
| 360 |
+
<span className="text-[11px] text-slate-500 block">Cắt xả tờ mẹ</span>
|
| 361 |
<span className="text-sm font-bold text-indigo-700">Chia {cutsPerParentSheet}</span>
|
| 362 |
<span className="text-[10px] text-slate-400 block">{parentSheet.widthCm}×{parentSheet.heightCm}cm</span>
|
| 363 |
</div>
|
| 364 |
|
| 365 |
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1">
|
| 366 |
+
<span className="text-[11px] text-slate-500 block">Tổng con / tờ mẹ</span>
|
| 367 |
+
<span className="text-sm font-bold text-violet-700">{totalUpsPerParentSheet} con</span>
|
| 368 |
+
<span className="text-[10px] text-slate-400 block">{upsPerPrintSheet} × {cutsPerParentSheet}</span>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<div className="bg-slate-50 border border-slate-200/80 rounded-lg py-2 px-1 col-span-3 sm:col-span-1">
|
| 372 |
<span className="text-[11px] text-slate-500 block">Tận dụng giấy</span>
|
| 373 |
<span className="text-sm font-bold text-emerald-700">{sheetEfficiencyPercent}%</span>
|
| 374 |
+
<span className="text-[10px] text-slate-400 block">Khổ {printW}×{printH}mm</span>
|
| 375 |
</div>
|
| 376 |
</div>
|
| 377 |
</div>
|
src/lib/engine/imposition.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { BoxCoordinate, ImpositionResult, PaperType, PrintTech } from '@/types';
|
| 2 |
|
| 3 |
interface ImpositionOptions {
|
| 4 |
widthMm: number;
|
|
@@ -10,6 +10,15 @@ interface ImpositionOptions {
|
|
| 10 |
gripperMarginMm?: number;
|
| 11 |
}
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
export function calculateImposition({
|
| 14 |
widthMm,
|
| 15 |
heightMm,
|
|
@@ -19,98 +28,103 @@ export function calculateImposition({
|
|
| 19 |
preferredMachineSheet,
|
| 20 |
gripperMarginMm = 10,
|
| 21 |
}: ImpositionOptions): ImpositionResult {
|
| 22 |
-
|
| 23 |
-
const
|
| 24 |
-
const itemH = heightMm + bleedMm * 2;
|
| 25 |
-
|
| 26 |
-
const parentW_mm = paperType.parentWidthCm * 10;
|
| 27 |
-
const parentH_mm = paperType.parentHeightCm * 10;
|
| 28 |
|
| 29 |
-
//
|
| 30 |
-
|
| 31 |
-
name: string;
|
| 32 |
-
widthMm: number;
|
| 33 |
-
heightMm: number;
|
| 34 |
-
cutsFromParent: number;
|
| 35 |
-
}[] = [];
|
| 36 |
|
| 37 |
if (preferredMachineSheet) {
|
| 38 |
-
// Máy in cụ thể được
|
| 39 |
-
const cuts = calculateCutsFromParent(
|
| 40 |
-
|
|
|
|
| 41 |
name: preferredMachineSheet.name,
|
| 42 |
-
|
| 43 |
-
|
| 44 |
cutsFromParent: cuts,
|
|
|
|
|
|
|
| 45 |
});
|
| 46 |
} else if (printTech === 'digital') {
|
| 47 |
-
// In nhanh
|
| 48 |
-
|
| 49 |
-
{ name: 'Khổ A3+ (330 x 488 mm)',
|
| 50 |
-
{ name: 'Khổ A3 chuẩn (297 x 420 mm)',
|
| 51 |
-
{ name: 'Khổ A4 chuẩn (210 x 297 mm)',
|
| 52 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
} else {
|
| 54 |
-
// In Offset:
|
| 55 |
-
//
|
| 56 |
-
if (Math.max(
|
| 57 |
-
const
|
| 58 |
-
const
|
| 59 |
-
|
| 60 |
-
name: `Khổ
|
| 61 |
-
|
| 62 |
-
|
| 63 |
cutsFromParent: 1,
|
|
|
|
|
|
|
| 64 |
});
|
| 65 |
}
|
| 66 |
|
| 67 |
-
//
|
| 68 |
-
|
| 69 |
-
const
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
});
|
| 76 |
|
| 77 |
-
//
|
| 78 |
-
|
| 79 |
-
const
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 86 |
});
|
| 87 |
-
// Cắt dọc
|
| 88 |
-
const cut2_2_w = Math.floor(parentW_mm / 2);
|
| 89 |
-
const cut2_2_h = parentH_mm;
|
| 90 |
-
if (cut2_2_w !== cut2_1_w || cut2_2_h !== cut2_1_h) {
|
| 91 |
-
candidatePrintSheets.push({
|
| 92 |
-
name: `Khổ chia 2 dọc (${cut2_2_w} x ${cut2_2_h} mm)`,
|
| 93 |
-
widthMm: cut2_2_w,
|
| 94 |
-
heightMm: cut2_2_h,
|
| 95 |
-
cutsFromParent: 2,
|
| 96 |
-
});
|
| 97 |
-
}
|
| 98 |
|
| 99 |
-
//
|
| 100 |
-
const
|
| 101 |
-
const
|
| 102 |
-
|
| 103 |
-
name: `Khổ chia 8 (${
|
| 104 |
-
|
| 105 |
-
|
| 106 |
cutsFromParent: 8,
|
|
|
|
|
|
|
| 107 |
});
|
| 108 |
}
|
| 109 |
|
| 110 |
-
// Đánh giá phương án
|
| 111 |
-
let
|
| 112 |
-
|
| 113 |
-
cutsFromParent: number;
|
| 114 |
ups: number;
|
| 115 |
cols: number;
|
| 116 |
rows: number;
|
|
@@ -121,50 +135,63 @@ export function calculateImposition({
|
|
| 121 |
usableH: number;
|
| 122 |
} | null = null;
|
| 123 |
|
| 124 |
-
for (const
|
| 125 |
-
const
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
itemW,
|
| 129 |
-
itemH,
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
}
|
| 148 |
}
|
| 149 |
}
|
| 150 |
|
| 151 |
-
// Fallback
|
| 152 |
-
if (!
|
| 153 |
-
const
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
cutsFromParent: fallbackSheet.cutsFromParent,
|
| 157 |
ups: 1,
|
| 158 |
cols: 1,
|
| 159 |
rows: 1,
|
| 160 |
isRotated: false,
|
| 161 |
-
boxes: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
efficiency: 10,
|
| 163 |
-
usableW:
|
| 164 |
-
usableH:
|
| 165 |
};
|
| 166 |
}
|
| 167 |
|
|
|
|
|
|
|
| 168 |
return {
|
| 169 |
parentSheet: {
|
| 170 |
widthCm: paperType.parentWidthCm,
|
|
@@ -172,61 +199,109 @@ export function calculateImposition({
|
|
| 172 |
name: `${paperType.name} (${paperType.parentWidthCm}x${paperType.parentHeightCm}cm)`,
|
| 173 |
},
|
| 174 |
printSheet: {
|
| 175 |
-
widthMm:
|
| 176 |
-
heightMm:
|
| 177 |
-
name:
|
| 178 |
},
|
| 179 |
-
upsPerPrintSheet:
|
| 180 |
-
cutsPerParentSheet:
|
| 181 |
-
totalUpsPerParentSheet:
|
| 182 |
-
gripperMarginMm,
|
| 183 |
-
usableWidthMm:
|
| 184 |
-
usableHeightMm:
|
| 185 |
-
cols
|
| 186 |
-
rows
|
| 187 |
-
isRotated
|
| 188 |
-
boxes
|
| 189 |
-
sheetEfficiencyPercent: Math.round(
|
|
|
|
| 190 |
};
|
| 191 |
}
|
| 192 |
|
| 193 |
-
//
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
if (usableW < itemW && usableW < itemH) {
|
| 206 |
return { ups: 0, cols: 0, rows: 0, isRotated: false, boxes: [], efficiency: 0, usableW, usableH };
|
| 207 |
}
|
| 208 |
|
| 209 |
-
// Cách 1: Xếp
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
const ups1 = cols1 * rows1;
|
| 213 |
|
| 214 |
-
// Cách 2: Xếp
|
| 215 |
-
|
| 216 |
-
const
|
|
|
|
| 217 |
const ups2 = cols2 * rows2;
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
const boxW = isRotated ? itemH : itemW;
|
| 224 |
const boxH = isRotated ? itemW : itemH;
|
| 225 |
|
| 226 |
-
//
|
| 227 |
-
const
|
| 228 |
-
const
|
| 229 |
-
const
|
|
|
|
| 230 |
|
| 231 |
let idx = 1;
|
| 232 |
for (let r = 0; r < bestRows; r++) {
|
|
@@ -237,13 +312,17 @@ function evaluateSheetImposition(
|
|
| 237 |
y: Math.round((startY + r * boxH) * 10) / 10,
|
| 238 |
w: boxW,
|
| 239 |
h: boxH,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
});
|
| 241 |
}
|
| 242 |
}
|
| 243 |
|
| 244 |
-
const
|
| 245 |
-
const sheetArea =
|
| 246 |
-
const efficiency = sheetArea > 0 ? (
|
| 247 |
|
| 248 |
return {
|
| 249 |
ups: bestUps,
|
|
@@ -257,9 +336,273 @@ function evaluateSheetImposition(
|
|
| 257 |
};
|
| 258 |
}
|
| 259 |
|
| 260 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
function calculateCutsFromParent(parentW: number, parentH: number, printW: number, printH: number): number {
|
| 262 |
-
// Thử 2 hướng cắt: thẳng và xoay
|
| 263 |
const cuts1 = Math.floor(parentW / printW) * Math.floor(parentH / printH);
|
| 264 |
const cuts2 = Math.floor(parentW / printH) * Math.floor(parentH / printW);
|
| 265 |
return Math.max(cuts1, cuts2, 1);
|
|
|
|
| 1 |
+
import { BoxCoordinate, ImpositionResult, PaperCutBlock, PaperCutLine, PaperCutScheme, PaperType, PrintTech } from '@/types';
|
| 2 |
|
| 3 |
interface ImpositionOptions {
|
| 4 |
widthMm: number;
|
|
|
|
| 10 |
gripperMarginMm?: number;
|
| 11 |
}
|
| 12 |
|
| 13 |
+
interface EvaluationCandidate {
|
| 14 |
+
name: string;
|
| 15 |
+
sheetWidthMm: number;
|
| 16 |
+
sheetHeightMm: number;
|
| 17 |
+
cutsFromParent: number;
|
| 18 |
+
cutScheme: PaperCutScheme;
|
| 19 |
+
gripperMarginMm: number;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
export function calculateImposition({
|
| 23 |
widthMm,
|
| 24 |
heightMm,
|
|
|
|
| 28 |
preferredMachineSheet,
|
| 29 |
gripperMarginMm = 10,
|
| 30 |
}: ImpositionOptions): ImpositionResult {
|
| 31 |
+
const parentW = paperType.parentWidthCm * 10;
|
| 32 |
+
const parentH = paperType.parentHeightCm * 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
// 1. Sinh danh sách các phương án cắt xả giấy mẹ
|
| 35 |
+
const candidates: EvaluationCandidate[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
if (preferredMachineSheet) {
|
| 38 |
+
// Máy in cụ thể được chỉ định
|
| 39 |
+
const cuts = calculateCutsFromParent(parentW, parentH, preferredMachineSheet.widthMm, preferredMachineSheet.heightMm);
|
| 40 |
+
const cutScheme = determineCutScheme(parentW, parentH, preferredMachineSheet.widthMm, preferredMachineSheet.heightMm, cuts);
|
| 41 |
+
candidates.push({
|
| 42 |
name: preferredMachineSheet.name,
|
| 43 |
+
sheetWidthMm: preferredMachineSheet.widthMm,
|
| 44 |
+
sheetHeightMm: preferredMachineSheet.heightMm,
|
| 45 |
cutsFromParent: cuts,
|
| 46 |
+
cutScheme,
|
| 47 |
+
gripperMarginMm,
|
| 48 |
});
|
| 49 |
} else if (printTech === 'digital') {
|
| 50 |
+
// In nhanh KTS: các khổ in máy tiêu chuẩn A3+, A3, A4
|
| 51 |
+
const digitalCandidates = [
|
| 52 |
+
{ name: 'Khổ A3+ (330 x 488 mm)', w: 488, h: 330, cuts: 4 },
|
| 53 |
+
{ name: 'Khổ A3 chuẩn (297 x 420 mm)', w: 420, h: 297, cuts: 4 },
|
| 54 |
+
{ name: 'Khổ A4 chuẩn (210 x 297 mm)', w: 297, h: 210, cuts: 8 },
|
| 55 |
];
|
| 56 |
+
|
| 57 |
+
for (const dc of digitalCandidates) {
|
| 58 |
+
const cuts = calculateCutsFromParent(parentW, parentH, dc.w, dc.h);
|
| 59 |
+
const cutScheme = determineCutScheme(parentW, parentH, dc.w, dc.h, cuts);
|
| 60 |
+
candidates.push({
|
| 61 |
+
name: dc.name,
|
| 62 |
+
sheetWidthMm: dc.w,
|
| 63 |
+
sheetHeightMm: dc.h,
|
| 64 |
+
cutsFromParent: cuts,
|
| 65 |
+
cutScheme,
|
| 66 |
+
gripperMarginMm: 5,
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
} else {
|
| 70 |
+
// In Offset: Các phương án chia khổ giấy mẹ chuẩn công nghiệp
|
| 71 |
+
// Phương án 0: Nguyên khổ (Chia 1) - ví dụ 65x86 (860 x 650 mm)
|
| 72 |
+
if (Math.max(parentW, parentH) <= 920) {
|
| 73 |
+
const w1 = Math.max(parentW, parentH);
|
| 74 |
+
const h1 = Math.min(parentW, parentH);
|
| 75 |
+
candidates.push({
|
| 76 |
+
name: `Khổ nguyên ${Math.round(parentW/10)}x${Math.round(parentH/10)}cm (Bắt nhíp ${w1/10}cm)`,
|
| 77 |
+
sheetWidthMm: w1,
|
| 78 |
+
sheetHeightMm: h1,
|
| 79 |
cutsFromParent: 1,
|
| 80 |
+
cutScheme: buildFullCutScheme(parentW, parentH),
|
| 81 |
+
gripperMarginMm,
|
| 82 |
});
|
| 83 |
}
|
| 84 |
|
| 85 |
+
// Phương án 1: Chia 2 (Cắt đôi)
|
| 86 |
+
// Cắt đôi chiều dài (thường tạo ra khổ 430 x 650 mm từ 65x86)
|
| 87 |
+
const longSide = Math.max(parentW, parentH);
|
| 88 |
+
const shortSide = Math.min(parentW, parentH);
|
| 89 |
+
const cut2_h = Math.floor(longSide / 2);
|
| 90 |
+
const cut2_w = shortSide;
|
| 91 |
+
candidates.push({
|
| 92 |
+
name: `Khổ chia 2 (${cut2_w} x ${cut2_h} mm)`,
|
| 93 |
+
sheetWidthMm: Math.max(cut2_w, cut2_h),
|
| 94 |
+
sheetHeightMm: Math.min(cut2_w, cut2_h),
|
| 95 |
+
cutsFromParent: 2,
|
| 96 |
+
cutScheme: buildCut2Scheme(parentW, parentH),
|
| 97 |
+
gripperMarginMm,
|
| 98 |
});
|
| 99 |
|
| 100 |
+
// Phương án 2: Chia 4 (Cắt chữ thập)
|
| 101 |
+
const cut4_w = Math.floor(parentW / 2);
|
| 102 |
+
const cut4_h = Math.floor(parentH / 2);
|
| 103 |
+
candidates.push({
|
| 104 |
+
name: `Khổ chia 4 (${cut4_w} x ${cut4_h} mm)`,
|
| 105 |
+
sheetWidthMm: Math.max(cut4_w, cut4_h),
|
| 106 |
+
sheetHeightMm: Math.min(cut4_w, cut4_h),
|
| 107 |
+
cutsFromParent: 4,
|
| 108 |
+
cutScheme: buildCut4Scheme(parentW, parentH),
|
| 109 |
+
gripperMarginMm,
|
| 110 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
// Phương án 3: Chia 8 (Cắt 8 - cho ấn phẩm nhỏ như namecard, tag...)
|
| 113 |
+
const cut8_1 = Math.floor(shortSide / 2);
|
| 114 |
+
const cut8_2 = Math.floor(longSide / 4);
|
| 115 |
+
candidates.push({
|
| 116 |
+
name: `Khổ chia 8 (${cut8_1} x ${cut8_2} mm)`,
|
| 117 |
+
sheetWidthMm: Math.max(cut8_1, cut8_2),
|
| 118 |
+
sheetHeightMm: Math.min(cut8_1, cut8_2),
|
| 119 |
cutsFromParent: 8,
|
| 120 |
+
cutScheme: buildCut8Scheme(parentW, parentH),
|
| 121 |
+
gripperMarginMm,
|
| 122 |
});
|
| 123 |
}
|
| 124 |
|
| 125 |
+
// 2. Đánh giá tất cả phương án để tìm phương án cho ra tổng số con trên tờ mẹ cao nhất
|
| 126 |
+
let bestCandidateResult: {
|
| 127 |
+
candidate: EvaluationCandidate;
|
|
|
|
| 128 |
ups: number;
|
| 129 |
cols: number;
|
| 130 |
rows: number;
|
|
|
|
| 135 |
usableH: number;
|
| 136 |
} | null = null;
|
| 137 |
|
| 138 |
+
for (const cand of candidates) {
|
| 139 |
+
const layout = evaluateImpositionLayout({
|
| 140 |
+
sheetW: cand.sheetWidthMm,
|
| 141 |
+
sheetH: cand.sheetHeightMm,
|
| 142 |
+
itemW: widthMm,
|
| 143 |
+
itemH: heightMm,
|
| 144 |
+
bleedMm,
|
| 145 |
+
gripperMarginMm: cand.gripperMarginMm,
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
const totalUps = layout.ups * cand.cutsFromParent;
|
| 149 |
+
|
| 150 |
+
if (!bestCandidateResult) {
|
| 151 |
+
bestCandidateResult = { candidate: cand, ...layout };
|
| 152 |
+
} else {
|
| 153 |
+
const currentBestTotal = bestCandidateResult.ups * bestCandidateResult.candidate.cutsFromParent;
|
| 154 |
+
if (totalUps > currentBestTotal) {
|
| 155 |
+
bestCandidateResult = { candidate: cand, ...layout };
|
| 156 |
+
} else if (totalUps === currentBestTotal) {
|
| 157 |
+
// Nếu tổng số con bằng nhau, ưu tiên phương án tờ in nhỏ hơn (chia 4 hoặc chia 2) để tiết kiệm chi phí in máy
|
| 158 |
+
if (cand.cutsFromParent > bestCandidateResult.candidate.cutsFromParent) {
|
| 159 |
+
bestCandidateResult = { candidate: cand, ...layout };
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
}
|
| 163 |
}
|
| 164 |
|
| 165 |
+
// Fallback nếu kh��ng có con nào vừa
|
| 166 |
+
if (!bestCandidateResult || bestCandidateResult.ups === 0) {
|
| 167 |
+
const fallbackCand = candidates[0];
|
| 168 |
+
bestCandidateResult = {
|
| 169 |
+
candidate: fallbackCand,
|
|
|
|
| 170 |
ups: 1,
|
| 171 |
cols: 1,
|
| 172 |
rows: 1,
|
| 173 |
isRotated: false,
|
| 174 |
+
boxes: [
|
| 175 |
+
{
|
| 176 |
+
index: 1,
|
| 177 |
+
x: 10,
|
| 178 |
+
y: 10,
|
| 179 |
+
w: widthMm + bleedMm * 2,
|
| 180 |
+
h: heightMm + bleedMm * 2,
|
| 181 |
+
trimW: widthMm,
|
| 182 |
+
trimH: heightMm,
|
| 183 |
+
bleedMm,
|
| 184 |
+
isRotated: false,
|
| 185 |
+
},
|
| 186 |
+
],
|
| 187 |
efficiency: 10,
|
| 188 |
+
usableW: fallbackCand.sheetWidthMm,
|
| 189 |
+
usableH: fallbackCand.sheetHeightMm,
|
| 190 |
};
|
| 191 |
}
|
| 192 |
|
| 193 |
+
const { candidate, ups, cols, rows, isRotated, boxes, efficiency, usableW, usableH } = bestCandidateResult;
|
| 194 |
+
|
| 195 |
return {
|
| 196 |
parentSheet: {
|
| 197 |
widthCm: paperType.parentWidthCm,
|
|
|
|
| 199 |
name: `${paperType.name} (${paperType.parentWidthCm}x${paperType.parentHeightCm}cm)`,
|
| 200 |
},
|
| 201 |
printSheet: {
|
| 202 |
+
widthMm: candidate.sheetWidthMm,
|
| 203 |
+
heightMm: candidate.sheetHeightMm,
|
| 204 |
+
name: candidate.name,
|
| 205 |
},
|
| 206 |
+
upsPerPrintSheet: ups,
|
| 207 |
+
cutsPerParentSheet: candidate.cutsFromParent,
|
| 208 |
+
totalUpsPerParentSheet: ups * candidate.cutsFromParent,
|
| 209 |
+
gripperMarginMm: candidate.gripperMarginMm,
|
| 210 |
+
usableWidthMm: usableW,
|
| 211 |
+
usableHeightMm: usableH,
|
| 212 |
+
cols,
|
| 213 |
+
rows,
|
| 214 |
+
isRotated,
|
| 215 |
+
boxes,
|
| 216 |
+
sheetEfficiencyPercent: Math.round(efficiency * 10) / 10,
|
| 217 |
+
cutScheme: candidate.cutScheme,
|
| 218 |
};
|
| 219 |
}
|
| 220 |
|
| 221 |
+
// -----------------------------------------------------------------------------------------
|
| 222 |
+
// THUẬT TOÁN XẾP BÌNH BÀI CHUẨN XÉNG CÔNG NGHIỆP TRÊN TỜ IN MÁY
|
| 223 |
+
// -----------------------------------------------------------------------------------------
|
| 224 |
+
interface LayoutEvalParams {
|
| 225 |
+
sheetW: number;
|
| 226 |
+
sheetH: number;
|
| 227 |
+
itemW: number;
|
| 228 |
+
itemH: number;
|
| 229 |
+
bleedMm: number;
|
| 230 |
+
gripperMarginMm: number;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
interface LayoutResult {
|
| 234 |
+
ups: number;
|
| 235 |
+
cols: number;
|
| 236 |
+
rows: number;
|
| 237 |
+
isRotated: boolean;
|
| 238 |
+
boxes: BoxCoordinate[];
|
| 239 |
+
efficiency: number;
|
| 240 |
+
usableW: number;
|
| 241 |
+
usableH: number;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function evaluateImpositionLayout({
|
| 245 |
+
sheetW,
|
| 246 |
+
sheetH,
|
| 247 |
+
itemW,
|
| 248 |
+
itemH,
|
| 249 |
+
bleedMm,
|
| 250 |
+
gripperMarginMm,
|
| 251 |
+
}: LayoutEvalParams): LayoutResult {
|
| 252 |
+
// Quy ước máy in:
|
| 253 |
+
// Cạnh dài là cạnh bắt nhíp: width = max(sheetW, sheetH), height = min(sheetW, sheetH)
|
| 254 |
+
const W = Math.max(sheetW, sheetH);
|
| 255 |
+
const H = Math.min(sheetW, sheetH);
|
| 256 |
+
|
| 257 |
+
// Lề kẹp nhíp ở cạnh đáy (gripperMarginMm = 10mm)
|
| 258 |
+
// Lề đuôi (đối diện nhíp): 4mm
|
| 259 |
+
// Lề 2 bên biên: 4mm mỗi bên (tổng 8mm)
|
| 260 |
+
const marginSide = 4;
|
| 261 |
+
const marginTail = 4;
|
| 262 |
+
const usableW = W - marginSide * 2;
|
| 263 |
+
const usableH = H - (gripperMarginMm + marginTail);
|
| 264 |
|
| 265 |
if (usableW < itemW && usableW < itemH) {
|
| 266 |
return { ups: 0, cols: 0, rows: 0, isRotated: false, boxes: [], efficiency: 0, usableW, usableH };
|
| 267 |
}
|
| 268 |
|
| 269 |
+
// --- Cách 1: Xếp Thẳng (Không xoay) ---
|
| 270 |
+
// Chiều ngang chứa C1 cột thành phẩm, có tràn lề 2mm ở 2 mép ngoài cùng
|
| 271 |
+
// Công thức: C1 * itemW + 2 * bleedMm <= usableW
|
| 272 |
+
const cols1 = Math.max(0, Math.floor((usableW - 2 * bleedMm) / itemW));
|
| 273 |
+
const rows1 = Math.max(0, Math.floor((usableH - 2 * bleedMm) / itemH));
|
| 274 |
const ups1 = cols1 * rows1;
|
| 275 |
|
| 276 |
+
// --- Cách 2: Xếp Xoay 90 độ ---
|
| 277 |
+
// Chiều ngang xếp theo itemH, chiều dọc xếp theo itemW
|
| 278 |
+
const cols2 = Math.max(0, Math.floor((usableW - 2 * bleedMm) / itemH));
|
| 279 |
+
const rows2 = Math.max(0, Math.floor((usableH - 2 * bleedMm) / itemW));
|
| 280 |
const ups2 = cols2 * rows2;
|
| 281 |
|
| 282 |
+
// Chọn phương án tốt nhất giữa Xếp Thẳng (ups1) và Xếp Xoay 90° (ups2)
|
| 283 |
+
let bestUps = ups1;
|
| 284 |
+
let bestCols = cols1;
|
| 285 |
+
let bestRows = rows1;
|
| 286 |
+
let isRotated = false;
|
| 287 |
+
|
| 288 |
+
if (ups2 > bestUps) {
|
| 289 |
+
bestUps = ups2;
|
| 290 |
+
bestCols = cols2;
|
| 291 |
+
bestRows = rows2;
|
| 292 |
+
isRotated = true;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Tạo danh sách tọa độ các hộp sản phẩm
|
| 296 |
+
const boxes: BoxCoordinate[] = [];
|
| 297 |
const boxW = isRotated ? itemH : itemW;
|
| 298 |
const boxH = isRotated ? itemW : itemH;
|
| 299 |
|
| 300 |
+
// Canh giữa cụm con trong vùng in hữu dụng
|
| 301 |
+
const blockW = bestCols * boxW + 2 * bleedMm;
|
| 302 |
+
const blockH = bestRows * boxH + 2 * bleedMm;
|
| 303 |
+
const startX = marginSide + Math.max(0, Math.floor((usableW - blockW) / 2)) + bleedMm;
|
| 304 |
+
const startY = marginTail + Math.max(0, Math.floor((usableH - blockH) / 2)) + bleedMm;
|
| 305 |
|
| 306 |
let idx = 1;
|
| 307 |
for (let r = 0; r < bestRows; r++) {
|
|
|
|
| 312 |
y: Math.round((startY + r * boxH) * 10) / 10,
|
| 313 |
w: boxW,
|
| 314 |
h: boxH,
|
| 315 |
+
trimW: itemW,
|
| 316 |
+
trimH: itemH,
|
| 317 |
+
bleedMm,
|
| 318 |
+
isRotated,
|
| 319 |
});
|
| 320 |
}
|
| 321 |
}
|
| 322 |
|
| 323 |
+
const totalItemArea = itemW * itemH * bestUps;
|
| 324 |
+
const sheetArea = W * H;
|
| 325 |
+
const efficiency = sheetArea > 0 ? (totalItemArea / sheetArea) * 100 : 0;
|
| 326 |
|
| 327 |
return {
|
| 328 |
ups: bestUps,
|
|
|
|
| 336 |
};
|
| 337 |
}
|
| 338 |
|
| 339 |
+
// -----------------------------------------------------------------------------------------
|
| 340 |
+
// SINH SƠ ĐỒ CẮT GIẤY MẸ TRỰC QUAN (PAPER CUTTING SCHEME)
|
| 341 |
+
// -----------------------------------------------------------------------------------------
|
| 342 |
+
|
| 343 |
+
export function determineCutScheme(
|
| 344 |
+
parentW: number,
|
| 345 |
+
parentH: number,
|
| 346 |
+
printW: number,
|
| 347 |
+
printH: number,
|
| 348 |
+
cuts: number
|
| 349 |
+
): PaperCutScheme {
|
| 350 |
+
const pw = Math.max(parentW, parentH);
|
| 351 |
+
const ph = Math.min(parentW, parentH);
|
| 352 |
+
|
| 353 |
+
if (cuts <= 1) return buildFullCutScheme(pw, ph);
|
| 354 |
+
if (cuts === 2) return buildCut2Scheme(pw, ph);
|
| 355 |
+
if (cuts === 4) return buildCut4Scheme(pw, ph);
|
| 356 |
+
if (cuts === 8) return buildCut8Scheme(pw, ph);
|
| 357 |
+
return buildSimpleCutScheme(pw, ph, printW, printH, cuts);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
function buildFullCutScheme(parentW: number, parentH: number): PaperCutScheme {
|
| 361 |
+
const pw = Math.max(parentW, parentH);
|
| 362 |
+
const ph = Math.min(parentW, parentH);
|
| 363 |
+
|
| 364 |
+
return {
|
| 365 |
+
parentWidthMm: pw,
|
| 366 |
+
parentHeightMm: ph,
|
| 367 |
+
cutType: 'chia_1',
|
| 368 |
+
cutDescription: `In nguyên khổ ${pw} x ${ph} mm (không cần xén trước khi in)`,
|
| 369 |
+
cutsCount: 1,
|
| 370 |
+
cutSheetWidthMm: pw,
|
| 371 |
+
cutSheetHeightMm: ph,
|
| 372 |
+
wasteAreaPercent: 0,
|
| 373 |
+
cutLines: [],
|
| 374 |
+
blocks: [
|
| 375 |
+
{
|
| 376 |
+
index: 1,
|
| 377 |
+
x: 0,
|
| 378 |
+
y: 0,
|
| 379 |
+
w: pw,
|
| 380 |
+
h: ph,
|
| 381 |
+
name: `Tờ in #1 (${pw} x ${ph} mm)`,
|
| 382 |
+
},
|
| 383 |
+
],
|
| 384 |
+
};
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
function buildCut2Scheme(parentW: number, parentH: number): PaperCutScheme {
|
| 388 |
+
const pw = Math.max(parentW, parentH); // ví dụ 860mm
|
| 389 |
+
const ph = Math.min(parentW, parentH); // ví dụ 650mm
|
| 390 |
+
const cutW = Math.floor(pw / 2); // 430mm
|
| 391 |
+
|
| 392 |
+
return {
|
| 393 |
+
parentWidthMm: pw,
|
| 394 |
+
parentHeightMm: ph,
|
| 395 |
+
cutType: 'chia_2',
|
| 396 |
+
cutDescription: `Xén đôi chiều ${pw}mm thành 2 tờ ${ph} x ${cutW} mm (Chia 2)`,
|
| 397 |
+
cutsCount: 2,
|
| 398 |
+
cutSheetWidthMm: ph,
|
| 399 |
+
cutSheetHeightMm: cutW,
|
| 400 |
+
wasteAreaPercent: 0,
|
| 401 |
+
cutLines: [
|
| 402 |
+
{
|
| 403 |
+
x1: cutW,
|
| 404 |
+
y1: 0,
|
| 405 |
+
x2: cutW,
|
| 406 |
+
y2: ph,
|
| 407 |
+
type: 'vertical',
|
| 408 |
+
label: `Xén dọc: ${cutW}mm`,
|
| 409 |
+
},
|
| 410 |
+
],
|
| 411 |
+
blocks: [
|
| 412 |
+
{
|
| 413 |
+
index: 1,
|
| 414 |
+
x: 0,
|
| 415 |
+
y: 0,
|
| 416 |
+
w: cutW,
|
| 417 |
+
h: ph,
|
| 418 |
+
name: `Tờ in #1 (${ph} x ${cutW} mm)`,
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
index: 2,
|
| 422 |
+
x: cutW,
|
| 423 |
+
y: 0,
|
| 424 |
+
w: pw - cutW,
|
| 425 |
+
h: ph,
|
| 426 |
+
name: `Tờ in #2 (${ph} x ${cutW} mm)`,
|
| 427 |
+
},
|
| 428 |
+
],
|
| 429 |
+
};
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
function buildCut4Scheme(parentW: number, parentH: number): PaperCutScheme {
|
| 433 |
+
const pw = Math.max(parentW, parentH); // ví dụ 860 hoặc 1090
|
| 434 |
+
const ph = Math.min(parentW, parentH); // ví dụ 650 hoặc 790
|
| 435 |
+
const halfW = Math.floor(pw / 2); // 430
|
| 436 |
+
const halfH = Math.floor(ph / 2); // 325
|
| 437 |
+
|
| 438 |
+
return {
|
| 439 |
+
parentWidthMm: pw,
|
| 440 |
+
parentHeightMm: ph,
|
| 441 |
+
cutType: 'chia_4',
|
| 442 |
+
cutDescription: `Xén chữ thập (Chia 4) thành 4 tờ ${halfW} x ${halfH} mm`,
|
| 443 |
+
cutsCount: 4,
|
| 444 |
+
cutSheetWidthMm: halfW,
|
| 445 |
+
cutSheetHeightMm: halfH,
|
| 446 |
+
wasteAreaPercent: 0,
|
| 447 |
+
cutLines: [
|
| 448 |
+
{
|
| 449 |
+
x1: halfW,
|
| 450 |
+
y1: 0,
|
| 451 |
+
x2: halfW,
|
| 452 |
+
y2: ph,
|
| 453 |
+
type: 'vertical',
|
| 454 |
+
label: `Xén dọc: ${halfW}mm`,
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
x1: 0,
|
| 458 |
+
y1: halfH,
|
| 459 |
+
x2: pw,
|
| 460 |
+
y2: halfH,
|
| 461 |
+
type: 'horizontal',
|
| 462 |
+
label: `Xén ngang: ${halfH}mm`,
|
| 463 |
+
},
|
| 464 |
+
],
|
| 465 |
+
blocks: [
|
| 466 |
+
{ index: 1, x: 0, y: 0, w: halfW, h: halfH, name: `Tờ in #1 (${halfW} x ${halfH} mm)` },
|
| 467 |
+
{ index: 2, x: halfW, y: 0, w: pw - halfW, h: halfH, name: `Tờ in #2 (${halfW} x ${halfH} mm)` },
|
| 468 |
+
{ index: 3, x: 0, y: halfH, w: halfW, h: ph - halfH, name: `Tờ in #3 (${halfW} x ${halfH} mm)` },
|
| 469 |
+
{ index: 4, x: halfW, y: halfH, w: pw - halfW, h: ph - halfH, name: `Tờ in #4 (${halfW} x ${halfH} mm)` },
|
| 470 |
+
],
|
| 471 |
+
};
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
function buildCut8Scheme(parentW: number, parentH: number): PaperCutScheme {
|
| 475 |
+
const pw = Math.max(parentW, parentH); // ví dụ 860
|
| 476 |
+
const ph = Math.min(parentW, parentH); // ví dụ 650
|
| 477 |
+
const colW = Math.floor(pw / 4); // 215
|
| 478 |
+
const rowH = Math.floor(ph / 2); // 325
|
| 479 |
+
|
| 480 |
+
const blocks: PaperCutBlock[] = [];
|
| 481 |
+
const cutLines: PaperCutLine[] = [
|
| 482 |
+
{ x1: colW, y1: 0, x2: colW, y2: ph, type: 'vertical', label: `Xén: ${colW}mm` },
|
| 483 |
+
{ x1: colW * 2, y1: 0, x2: colW * 2, y2: ph, type: 'vertical', label: `Xén: ${colW * 2}mm` },
|
| 484 |
+
{ x1: colW * 3, y1: 0, x2: colW * 3, y2: ph, type: 'vertical', label: `Xén: ${colW * 3}mm` },
|
| 485 |
+
{ x1: 0, y1: rowH, x2: pw, y2: rowH, type: 'horizontal', label: `Xén ngang: ${rowH}mm` },
|
| 486 |
+
];
|
| 487 |
+
|
| 488 |
+
let idx = 1;
|
| 489 |
+
for (let r = 0; r < 2; r++) {
|
| 490 |
+
for (let c = 0; c < 4; c++) {
|
| 491 |
+
const curW = c === 3 ? pw - colW * 3 : colW;
|
| 492 |
+
const curH = r === 1 ? ph - rowH : rowH;
|
| 493 |
+
blocks.push({
|
| 494 |
+
index: idx++,
|
| 495 |
+
x: c * colW,
|
| 496 |
+
y: r * rowH,
|
| 497 |
+
w: curW,
|
| 498 |
+
h: curH,
|
| 499 |
+
name: `Tờ in #${idx - 1} (${rowH} x ${colW} mm)`,
|
| 500 |
+
});
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
return {
|
| 505 |
+
parentWidthMm: pw,
|
| 506 |
+
parentHeightMm: ph,
|
| 507 |
+
cutType: 'chia_8',
|
| 508 |
+
cutDescription: `Xén chia 8 thành 8 tờ ${rowH} x ${colW} mm`,
|
| 509 |
+
cutsCount: 8,
|
| 510 |
+
cutSheetWidthMm: rowH,
|
| 511 |
+
cutSheetHeightMm: colW,
|
| 512 |
+
wasteAreaPercent: 0,
|
| 513 |
+
cutLines,
|
| 514 |
+
blocks,
|
| 515 |
+
};
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
function buildSimpleCutScheme(
|
| 519 |
+
parentW: number,
|
| 520 |
+
parentH: number,
|
| 521 |
+
printW: number,
|
| 522 |
+
printH: number,
|
| 523 |
+
cuts: number
|
| 524 |
+
): PaperCutScheme {
|
| 525 |
+
const pw = Math.max(parentW, parentH);
|
| 526 |
+
const ph = Math.min(parentW, parentH);
|
| 527 |
+
|
| 528 |
+
// So sánh 2 chiều xếp trên tờ mẹ
|
| 529 |
+
const c1 = Math.floor(pw / printW);
|
| 530 |
+
const r1 = Math.floor(ph / printH);
|
| 531 |
+
const tot1 = c1 * r1;
|
| 532 |
+
|
| 533 |
+
const c2 = Math.floor(pw / printH);
|
| 534 |
+
const r2 = Math.floor(ph / printW);
|
| 535 |
+
const tot2 = c2 * r2;
|
| 536 |
+
|
| 537 |
+
const useRotated = tot2 > tot1;
|
| 538 |
+
const pieceW = useRotated ? printH : printW;
|
| 539 |
+
const pieceH = useRotated ? printW : printH;
|
| 540 |
+
const cols = useRotated ? c2 : c1;
|
| 541 |
+
const rows = useRotated ? r2 : r1;
|
| 542 |
+
|
| 543 |
+
const actualCols = Math.max(1, cols);
|
| 544 |
+
const actualRows = Math.max(1, rows);
|
| 545 |
+
|
| 546 |
+
const blocks: PaperCutBlock[] = [];
|
| 547 |
+
const cutLines: PaperCutLine[] = [];
|
| 548 |
+
|
| 549 |
+
let idx = 1;
|
| 550 |
+
for (let r = 0; r < actualRows; r++) {
|
| 551 |
+
for (let c = 0; c < actualCols; c++) {
|
| 552 |
+
if (idx <= cuts) {
|
| 553 |
+
blocks.push({
|
| 554 |
+
index: idx++,
|
| 555 |
+
x: c * pieceW,
|
| 556 |
+
y: r * pieceH,
|
| 557 |
+
w: pieceW,
|
| 558 |
+
h: pieceH,
|
| 559 |
+
name: `Tờ in #${idx - 1} (${pieceW} x ${pieceH} mm)`,
|
| 560 |
+
});
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
// Tạo cut lines
|
| 566 |
+
for (let c = 1; c < actualCols; c++) {
|
| 567 |
+
cutLines.push({
|
| 568 |
+
x1: c * pieceW,
|
| 569 |
+
y1: 0,
|
| 570 |
+
x2: c * pieceW,
|
| 571 |
+
y2: Math.min(ph, actualRows * pieceH),
|
| 572 |
+
type: 'vertical',
|
| 573 |
+
label: `Xén: ${c * pieceW}mm`,
|
| 574 |
+
});
|
| 575 |
+
}
|
| 576 |
+
for (let r = 1; r < actualRows; r++) {
|
| 577 |
+
cutLines.push({
|
| 578 |
+
x1: 0,
|
| 579 |
+
y1: r * pieceH,
|
| 580 |
+
x2: Math.min(pw, actualCols * pieceW),
|
| 581 |
+
y2: r * pieceH,
|
| 582 |
+
type: 'horizontal',
|
| 583 |
+
label: `Xén: ${r * pieceH}mm`,
|
| 584 |
+
});
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
const usedArea = cuts * pieceW * pieceH;
|
| 588 |
+
const totalArea = pw * ph;
|
| 589 |
+
const wastePercent = totalArea > 0 ? Math.max(0, Math.round(((totalArea - usedArea) / totalArea) * 100)) : 0;
|
| 590 |
+
|
| 591 |
+
return {
|
| 592 |
+
parentWidthMm: pw,
|
| 593 |
+
parentHeightMm: ph,
|
| 594 |
+
cutType: cuts === 2 ? 'chia_2' : cuts === 4 ? 'chia_4' : cuts === 8 ? 'chia_8' : 'custom',
|
| 595 |
+
cutDescription: `Cắt ${cuts} tờ in (${pieceW} x ${pieceH} mm) từ tờ giấy mẹ ${pw} x ${ph} mm`,
|
| 596 |
+
cutsCount: cuts,
|
| 597 |
+
cutSheetWidthMm: pieceW,
|
| 598 |
+
cutSheetHeightMm: pieceH,
|
| 599 |
+
wasteAreaPercent: wastePercent,
|
| 600 |
+
cutLines,
|
| 601 |
+
blocks,
|
| 602 |
+
};
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
function calculateCutsFromParent(parentW: number, parentH: number, printW: number, printH: number): number {
|
|
|
|
| 606 |
const cuts1 = Math.floor(parentW / printW) * Math.floor(parentH / printH);
|
| 607 |
const cuts2 = Math.floor(parentW / printH) * Math.floor(parentH / printW);
|
| 608 |
return Math.max(cuts1, cuts2, 1);
|
src/types/index.ts
CHANGED
|
@@ -101,11 +101,46 @@ export interface CalculationInput {
|
|
| 101 |
vatPercent?: number;
|
| 102 |
}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
export interface BoxCoordinate {
|
| 105 |
x: number;
|
| 106 |
y: number;
|
| 107 |
w: number;
|
| 108 |
h: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
index: number;
|
| 110 |
}
|
| 111 |
|
|
@@ -131,6 +166,7 @@ export interface ImpositionResult {
|
|
| 131 |
isRotated: boolean;
|
| 132 |
boxes: BoxCoordinate[];
|
| 133 |
sheetEfficiencyPercent: number;
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
export interface FinishingCostDetail {
|
|
|
|
| 101 |
vatPercent?: number;
|
| 102 |
}
|
| 103 |
|
| 104 |
+
export interface PaperCutLine {
|
| 105 |
+
x1: number;
|
| 106 |
+
y1: number;
|
| 107 |
+
x2: number;
|
| 108 |
+
y2: number;
|
| 109 |
+
type: 'vertical' | 'horizontal';
|
| 110 |
+
label?: string;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
export interface PaperCutBlock {
|
| 114 |
+
index: number;
|
| 115 |
+
x: number;
|
| 116 |
+
y: number;
|
| 117 |
+
w: number;
|
| 118 |
+
h: number;
|
| 119 |
+
name: string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export interface PaperCutScheme {
|
| 123 |
+
parentWidthMm: number;
|
| 124 |
+
parentHeightMm: number;
|
| 125 |
+
cutType: 'chia_1' | 'chia_2' | 'chia_4' | 'chia_6' | 'chia_8' | 'custom';
|
| 126 |
+
cutDescription: string;
|
| 127 |
+
cutsCount: number;
|
| 128 |
+
cutSheetWidthMm: number;
|
| 129 |
+
cutSheetHeightMm: number;
|
| 130 |
+
blocks: PaperCutBlock[];
|
| 131 |
+
wasteAreaPercent: number;
|
| 132 |
+
cutLines?: PaperCutLine[];
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
export interface BoxCoordinate {
|
| 136 |
x: number;
|
| 137 |
y: number;
|
| 138 |
w: number;
|
| 139 |
h: number;
|
| 140 |
+
trimW?: number;
|
| 141 |
+
trimH?: number;
|
| 142 |
+
bleedMm?: number;
|
| 143 |
+
isRotated?: boolean;
|
| 144 |
index: number;
|
| 145 |
}
|
| 146 |
|
|
|
|
| 166 |
isRotated: boolean;
|
| 167 |
boxes: BoxCoordinate[];
|
| 168 |
sheetEfficiencyPercent: number;
|
| 169 |
+
cutScheme?: PaperCutScheme;
|
| 170 |
}
|
| 171 |
|
| 172 |
export interface FinishingCostDetail {
|