175 lines
7.4 KiB
TypeScript
175 lines
7.4 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useMemo, useState } from "react";
|
|||
|
|
import ToolShell from "@/components/toolkit/ToolShell";
|
|||
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|||
|
|
import { Input } from "@/components/ui/input";
|
|||
|
|
|
|||
|
|
function num(v: string) {
|
|||
|
|
const n = parseFloat(v);
|
|||
|
|
return Number.isFinite(n) ? n : 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fmtTime(seconds: number) {
|
|||
|
|
if (!Number.isFinite(seconds) || seconds <= 0) return "0 s";
|
|||
|
|
const s = Math.round(seconds);
|
|||
|
|
const m = Math.floor(s / 60);
|
|||
|
|
const rem = s % 60;
|
|||
|
|
if (m < 60) return `${m}m ${rem}s`;
|
|||
|
|
const h = Math.floor(m / 60);
|
|||
|
|
const mm = m % 60;
|
|||
|
|
return `${h}h ${mm}m`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function Page() {
|
|||
|
|
const [mode, setMode] = useState<"raster" | "vector">("raster");
|
|||
|
|
const [passes, setPasses] = useState("1");
|
|||
|
|
|
|||
|
|
// raster
|
|||
|
|
const [width, setWidth] = useState("100"); // mm
|
|||
|
|
const [height, setHeight] = useState("100");// mm
|
|||
|
|
const [dpi, setDpi] = useState("300");
|
|||
|
|
const [speedRaster, setSpeedRaster] = useState("800"); // mm/s
|
|||
|
|
const [overheadR, setOverheadR] = useState("1.10"); // factor
|
|||
|
|
|
|||
|
|
// vector
|
|||
|
|
const [length, setLength] = useState("500"); // mm
|
|||
|
|
const [speedVector, setSpeedVector] = useState("50"); // mm/s
|
|||
|
|
const [overheadV, setOverheadV] = useState("1.05"); // factor
|
|||
|
|
|
|||
|
|
const computed = useMemo(() => {
|
|||
|
|
const p = Math.max(1, Math.round(num(passes)));
|
|||
|
|
if (mode === "raster") {
|
|||
|
|
const w = num(width), h = num(height), D = num(dpi), v = num(speedRaster), k = Math.max(0.5, num(overheadR));
|
|||
|
|
if (w <= 0 || h <= 0 || D <= 0 || v <= 0) return { t: 0, gapMm: 0, gapUm: 0, rows: 0 };
|
|||
|
|
const gapMm = 25.4 / D;
|
|||
|
|
const gapUm = gapMm * 1000;
|
|||
|
|
const rows = h / gapMm;
|
|||
|
|
const t = rows * (w / v) * p * k;
|
|||
|
|
return { t, gapMm, gapUm, rows };
|
|||
|
|
} else {
|
|||
|
|
const L = num(length), v = num(speedVector), k = Math.max(0.5, num(overheadV));
|
|||
|
|
if (L <= 0 || v <= 0) return { t: 0, gapMm: 0, gapUm: 0, rows: 0 };
|
|||
|
|
const t = (L / v) * Math.max(1, Math.round(num(passes))) * k;
|
|||
|
|
return { t, gapMm: 0, gapUm: 0, rows: 0 };
|
|||
|
|
}
|
|||
|
|
}, [mode, passes, width, height, dpi, speedRaster, overheadR, length, speedVector, overheadV]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<ToolShell title="Job Time Estimator">
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">Mode</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="grid gap-3 sm:grid-cols-4">
|
|||
|
|
<label className="text-[11px] sm:text-xs col-span-2 sm:col-span-1">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Type</div>
|
|||
|
|
<select
|
|||
|
|
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|||
|
|
value={mode}
|
|||
|
|
onChange={(e) => (setMode(e.target.value as any))}
|
|||
|
|
>
|
|||
|
|
<option value="raster">Raster</option>
|
|||
|
|
<option value="vector">Vector</option>
|
|||
|
|
</select>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Passes</div>
|
|||
|
|
<Input inputMode="numeric" value={passes} onChange={(e) => setPasses(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{mode === "raster" ? (
|
|||
|
|
<Card className="mt-4">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">Raster Inputs</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="grid gap-3 sm:grid-cols-5">
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Width (mm)</div>
|
|||
|
|
<Input value={width} onChange={(e) => setWidth(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Height (mm)</div>
|
|||
|
|
<Input value={height} onChange={(e) => setHeight(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">DPI</div>
|
|||
|
|
<Input value={dpi} onChange={(e) => setDpi(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Speed (mm/s)</div>
|
|||
|
|
<Input value={speedRaster} onChange={(e) => setSpeedRaster(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Overhead factor</div>
|
|||
|
|
<Input value={overheadR} onChange={(e) => setOverheadR(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
) : (
|
|||
|
|
<Card className="mt-4">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">Vector Inputs</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Total path length (mm)</div>
|
|||
|
|
<Input value={length} onChange={(e) => setLength(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Speed (mm/s)</div>
|
|||
|
|
<Input value={speedVector} onChange={(e) => setSpeedVector(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
<label className="text-[11px] sm:text-xs">
|
|||
|
|
<div className="mb-1 text-muted-foreground">Overhead factor</div>
|
|||
|
|
<Input value={overheadV} onChange={(e) => setOverheadV(e.target.value)} />
|
|||
|
|
</label>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Card className="mt-4">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">Estimate</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">Estimated time</div>
|
|||
|
|
<div className="text-lg">{fmtTime(computed.t)}</div>
|
|||
|
|
</div>
|
|||
|
|
{mode === "raster" && (
|
|||
|
|
<>
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">Scan gap</div>
|
|||
|
|
<div className="text-lg">{computed.gapMm.toFixed(4)} mm</div>
|
|||
|
|
<div className="text-xs text-muted-foreground">{computed.gapUm.toFixed(1)} µm</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">Line count</div>
|
|||
|
|
<div className="text-lg">{computed.rows.toFixed(0)}</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Footnote */}
|
|||
|
|
<p className="mt-4 text-xs leading-relaxed text-muted-foreground">
|
|||
|
|
<span className="font-semibold">Overhead factor*</span> accounts for real-world slowdowns:
|
|||
|
|
acceleration/decelleration, jump moves, polygon delays, laser on/off timing, overscan,
|
|||
|
|
bidirectional settle time, and controller latency.{" "}
|
|||
|
|
<span className="font-semibold">Typical values:</span> Vector cuts/marks{" "}
|
|||
|
|
<span className="font-medium">1.05–1.15</span> (simple paths, long runs closer to 1.05; tiny
|
|||
|
|
segments or lots of jumps closer to 1.15). Raster engraving{" "}
|
|||
|
|
<span className="font-medium">1.10–1.40</span> (lower DPI and long sweeps near 1.10;
|
|||
|
|
very high DPI or short scan width near 1.30–1.40). Galvo systems often have lower overhead
|
|||
|
|
at small sizes; gantry systems tend to have higher overhead at high DPI/short strokes.
|
|||
|
|
</p>
|
|||
|
|
</ToolShell>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|