174 lines
7.4 KiB
TypeScript
174 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>
|
||
);
|
||
}
|
||
|