feat(scale): add mm/cm/m/km unit switcher for real dimensions

Scale calculator now supports all 4 real-world units:
- mm, cm, m, km — toggle buttons next to mode selector
- Formula adapts via unit→mm multiplier (mm=1, cm=10, m=1000, km=1M)
- Real→Desen: input in chosen unit, output always mm on drawing
- Desen→Real: output in chosen unit, secondary line shows all other units
- Switching unit clears input to avoid confusion
- step attribute adapts per unit (km=0.001, m=0.01, others=0.5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-12 23:57:28 +02:00
parent 75a7ab91ca
commit 22eb9a4383
@@ -2536,39 +2536,61 @@ const SCALE_PRESETS = [
{ label: "1:5000", value: 5000 },
];
// Real unit → multiplier to convert to mm
const REAL_UNITS = [
{ id: "mm", label: "mm", toMm: 1 },
{ id: "cm", label: "cm", toMm: 10 },
{ id: "m", label: "m", toMm: 1_000 },
{ id: "km", label: "km", toMm: 1_000_000 },
] as const;
type RealUnit = (typeof REAL_UNITS)[number]["id"];
/**
* Scale calculator — all real dimensions in cm, drawing in mm.
* Scale calculator.
*
* Real → Desen: drawing_mm = real_cm × 10 / scale
* e.g. 350 cm at 1:100 → 350 × 10 / 100 = 35 mm
* Core identity (all in mm):
* drawing_mm = real_mm / scale
* real_mm = drawing_mm × scale
*
* Desen → Real: real_cm = drawing_mm × scale / 10
* e.g. 35 mm at 1:100 → 35 × 100 / 10 = 350 cm = 3.50 m
* Real input in any unit → convert to mm → apply scale → drawing in mm.
* Drawing input in mm → apply scale → real mm → convert to chosen unit.
*/
function ScaleCalculator() {
const [scale, setScale] = useState(100);
const [customScale, setCustomScale] = useState("");
const [mode, setMode] = useState<"real-to-drawing" | "drawing-to-real">(
"real-to-drawing",
);
const [mode, setMode] = useState<"real-to-drawing" | "drawing-to-real">("real-to-drawing");
const [realUnit, setRealUnit] = useState<RealUnit>("cm");
const [inputVal, setInputVal] = useState("");
const effectiveScale =
customScale !== "" ? parseFloat(customScale) || scale : scale;
const effectiveScale = customScale !== "" ? parseFloat(customScale) || scale : scale;
const val = parseFloat(inputVal);
// drawing_mm = real_cm × 10 / scale
// real_cm = drawing_mm × scale / 10
const result = !isNaN(val)
? mode === "real-to-drawing"
? (val * 10) / effectiveScale // cm → mm on drawing
: (val * effectiveScale) / 10 // mm drawing → cm real
const unitDef = REAL_UNITS.find((u) => u.id === realUnit)!;
// drawing_mm = real_in_unit × unitDef.toMm / scale
// real_in_unit = drawing_mm × scale / unitDef.toMm
const drawing_mm = !isNaN(val) && mode === "real-to-drawing"
? (val * unitDef.toMm) / effectiveScale
: NaN;
const real_in_unit = !isNaN(val) && mode === "drawing-to-real"
? (val * effectiveScale) / unitDef.toMm
: NaN;
const fmt2 = (n: number) =>
isNaN(n)
// For secondary display: real_mm (whichever mode)
const real_mm = mode === "real-to-drawing"
? val * unitDef.toMm
: val * effectiveScale;
const fmt = (n: number, decimals = 2) =>
isNaN(n) || !isFinite(n)
? "—"
: n.toLocaleString("ro-RO", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
: n.toLocaleString("ro-RO", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
// Build secondary real conversions (exclude current unit)
const secondaryReal = (mm: number) =>
REAL_UNITS.filter((u) => u.id !== realUnit)
.map((u) => `${fmt(mm / u.toMm)} ${u.id}`)
.join(" · ");
return (
<div className="space-y-4">
@@ -2601,43 +2623,64 @@ function ScaleCalculator() {
</div>
</div>
{/* Mode */}
<div className="flex gap-2">
<Button
variant={mode === "real-to-drawing" ? "default" : "outline"}
size="sm"
onClick={() => { setMode("real-to-drawing"); setInputVal(""); }}
>
Real Desen
</Button>
<Button
variant={mode === "drawing-to-real" ? "default" : "outline"}
size="sm"
onClick={() => { setMode("drawing-to-real"); setInputVal(""); }}
>
Desen Real
</Button>
{/* Mode + real unit picker */}
<div className="flex flex-wrap gap-3 items-center">
<div className="flex gap-2">
<Button
variant={mode === "real-to-drawing" ? "default" : "outline"}
size="sm"
onClick={() => { setMode("real-to-drawing"); setInputVal(""); }}
>
Real Desen
</Button>
<Button
variant={mode === "drawing-to-real" ? "default" : "outline"}
size="sm"
onClick={() => { setMode("drawing-to-real"); setInputVal(""); }}
>
Desen Real
</Button>
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground">Unitate reală:</span>
{REAL_UNITS.map((u) => (
<Button
key={u.id}
type="button"
variant={realUnit === u.id ? "secondary" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
onClick={() => { setRealUnit(u.id); setInputVal(""); }}
>
{u.label}
</Button>
))}
</div>
</div>
{/* Input */}
<div>
<Label>
{mode === "real-to-drawing"
? "Dimensiune reală (cm)"
? `Dimensiune reală (${realUnit})`
: "Dimensiune desen (mm)"}
</Label>
<div className="mt-1 flex gap-2 items-center">
<Input
type="number"
min={0}
step={0.5}
step={realUnit === "km" ? 0.001 : realUnit === "m" ? 0.01 : 0.5}
value={inputVal}
onChange={(e) => setInputVal(e.target.value)}
placeholder={mode === "real-to-drawing" ? "ex: 350" : "ex: 35"}
placeholder={
mode === "real-to-drawing"
? realUnit === "m" ? "ex: 3.5" : realUnit === "km" ? "ex: 0.350" : "ex: 350"
: "ex: 35"
}
className="flex-1"
/>
<span className="text-sm text-muted-foreground shrink-0">
{mode === "real-to-drawing" ? "cm" : "mm"}
<span className="text-sm text-muted-foreground shrink-0 w-8">
{mode === "real-to-drawing" ? realUnit : "mm"}
</span>
</div>
</div>
@@ -2645,41 +2688,42 @@ function ScaleCalculator() {
{/* Result */}
{!isNaN(val) && val > 0 && (
<div className="rounded-md border bg-muted/30 p-4 space-y-2 text-sm">
<p className="text-xs text-muted-foreground font-medium">
Scară 1:{effectiveScale}
</p>
<p className="text-xs text-muted-foreground font-medium">Scară 1:{effectiveScale}</p>
{mode === "real-to-drawing" ? (
<>
<p>
Real: <strong>{fmt2(val)} cm</strong>{" "}
<span className="text-muted-foreground">
({fmt2(val / 100)} m)
</span>
Real:{" "}
<strong>{fmt(val)} {realUnit}</strong>
{realUnit !== "mm" && (
<span className="text-xs text-muted-foreground ml-2">
({secondaryReal(real_mm)})
</span>
)}
</p>
<p className="text-base border-t pt-2">
Pe desen:{" "}
<strong className="text-primary">{fmt2(result)} mm</strong>
<CopyButton text={fmt2(result)} />
<strong className="text-primary">{fmt(drawing_mm)} mm</strong>
<CopyButton text={fmt(drawing_mm)} />
</p>
{!isNaN(result) && (
{!isNaN(drawing_mm) && (
<p className="text-xs text-muted-foreground">
= {fmt2(result / 10)} cm pe desen
= {fmt(drawing_mm / 10)} cm pe desen
</p>
)}
</>
) : (
<>
<p>
Pe desen: <strong>{fmt2(val)} mm</strong>
Pe desen: <strong>{fmt(val)} mm</strong>
</p>
<p className="text-base border-t pt-2">
Real:{" "}
<strong className="text-primary">{fmt2(result)} cm</strong>
<CopyButton text={fmt2(result)} />
<strong className="text-primary">{fmt(real_in_unit)} {realUnit}</strong>
<CopyButton text={fmt(real_in_unit)} />
</p>
{!isNaN(result) && (
{!isNaN(real_mm) && (
<p className="text-xs text-muted-foreground">
= {fmt2(result / 100)} m &nbsp;·&nbsp; {fmt2(result * 10)} mm
{secondaryReal(real_mm)}
</p>
)}
</>