makearmy-app/app/laser-toolkit/job-time-estimator/page.tsx
2025-09-22 10:37:53 -04:00

174 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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.051.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.101.40</span> (lower DPI and long sweeps near 1.10;
very high DPI or short scan width near 1.301.40). Galvo systems often have lower overhead
at small sizes; gantry systems tend to have higher overhead at high DPI/short strokes.
</p>
</ToolShell>
);
}