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 }, { 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 * Core identity (all in mm):
* e.g. 350 cm at 1:100 → 350 × 10 / 100 = 35 mm * drawing_mm = real_mm / scale
* real_mm = drawing_mm × scale
* *
* Desen → Real: real_cm = drawing_mm × scale / 10 * Real input in any unit → convert to mm → apply scale → drawing in mm.
* e.g. 35 mm at 1:100 → 35 × 100 / 10 = 350 cm = 3.50 m * Drawing input in mm → apply scale → real mm → convert to chosen unit.
*/ */
function ScaleCalculator() { function ScaleCalculator() {
const [scale, setScale] = useState(100); const [scale, setScale] = useState(100);
const [customScale, setCustomScale] = useState(""); const [customScale, setCustomScale] = useState("");
const [mode, setMode] = useState<"real-to-drawing" | "drawing-to-real">( const [mode, setMode] = useState<"real-to-drawing" | "drawing-to-real">("real-to-drawing");
"real-to-drawing", const [realUnit, setRealUnit] = useState<RealUnit>("cm");
);
const [inputVal, setInputVal] = useState(""); const [inputVal, setInputVal] = useState("");
const effectiveScale = const effectiveScale = customScale !== "" ? parseFloat(customScale) || scale : scale;
customScale !== "" ? parseFloat(customScale) || scale : scale;
const val = parseFloat(inputVal); const val = parseFloat(inputVal);
// drawing_mm = real_cm × 10 / scale const unitDef = REAL_UNITS.find((u) => u.id === realUnit)!;
// real_cm = drawing_mm × scale / 10
const result = !isNaN(val) // drawing_mm = real_in_unit × unitDef.toMm / scale
? mode === "real-to-drawing" // real_in_unit = drawing_mm × scale / unitDef.toMm
? (val * 10) / effectiveScale // cm → mm on drawing const drawing_mm = !isNaN(val) && mode === "real-to-drawing"
: (val * effectiveScale) / 10 // mm drawing → cm real ? (val * unitDef.toMm) / effectiveScale
: NaN;
const real_in_unit = !isNaN(val) && mode === "drawing-to-real"
? (val * effectiveScale) / unitDef.toMm
: NaN; : NaN;
const fmt2 = (n: number) => // For secondary display: real_mm (whichever mode)
isNaN(n) 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -2601,43 +2623,64 @@ function ScaleCalculator() {
</div> </div>
</div> </div>
{/* Mode */} {/* Mode + real unit picker */}
<div className="flex gap-2"> <div className="flex flex-wrap gap-3 items-center">
<Button <div className="flex gap-2">
variant={mode === "real-to-drawing" ? "default" : "outline"} <Button
size="sm" variant={mode === "real-to-drawing" ? "default" : "outline"}
onClick={() => { setMode("real-to-drawing"); setInputVal(""); }} size="sm"
> onClick={() => { setMode("real-to-drawing"); setInputVal(""); }}
Real Desen >
</Button> Real Desen
<Button </Button>
variant={mode === "drawing-to-real" ? "default" : "outline"} <Button
size="sm" variant={mode === "drawing-to-real" ? "default" : "outline"}
onClick={() => { setMode("drawing-to-real"); setInputVal(""); }} size="sm"
> onClick={() => { setMode("drawing-to-real"); setInputVal(""); }}
Desen Real >
</Button> 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> </div>
{/* Input */} {/* Input */}
<div> <div>
<Label> <Label>
{mode === "real-to-drawing" {mode === "real-to-drawing"
? "Dimensiune reală (cm)" ? `Dimensiune reală (${realUnit})`
: "Dimensiune desen (mm)"} : "Dimensiune desen (mm)"}
</Label> </Label>
<div className="mt-1 flex gap-2 items-center"> <div className="mt-1 flex gap-2 items-center">
<Input <Input
type="number" type="number"
min={0} min={0}
step={0.5} step={realUnit === "km" ? 0.001 : realUnit === "m" ? 0.01 : 0.5}
value={inputVal} value={inputVal}
onChange={(e) => setInputVal(e.target.value)} 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" className="flex-1"
/> />
<span className="text-sm text-muted-foreground shrink-0"> <span className="text-sm text-muted-foreground shrink-0 w-8">
{mode === "real-to-drawing" ? "cm" : "mm"} {mode === "real-to-drawing" ? realUnit : "mm"}
</span> </span>
</div> </div>
</div> </div>
@@ -2645,41 +2688,42 @@ function ScaleCalculator() {
{/* Result */} {/* Result */}
{!isNaN(val) && val > 0 && ( {!isNaN(val) && val > 0 && (
<div className="rounded-md border bg-muted/30 p-4 space-y-2 text-sm"> <div className="rounded-md border bg-muted/30 p-4 space-y-2 text-sm">
<p className="text-xs text-muted-foreground font-medium"> <p className="text-xs text-muted-foreground font-medium">Scară 1:{effectiveScale}</p>
Scară 1:{effectiveScale}
</p>
{mode === "real-to-drawing" ? ( {mode === "real-to-drawing" ? (
<> <>
<p> <p>
Real: <strong>{fmt2(val)} cm</strong>{" "} Real:{" "}
<span className="text-muted-foreground"> <strong>{fmt(val)} {realUnit}</strong>
({fmt2(val / 100)} m) {realUnit !== "mm" && (
</span> <span className="text-xs text-muted-foreground ml-2">
({secondaryReal(real_mm)})
</span>
)}
</p> </p>
<p className="text-base border-t pt-2"> <p className="text-base border-t pt-2">
Pe desen:{" "} Pe desen:{" "}
<strong className="text-primary">{fmt2(result)} mm</strong> <strong className="text-primary">{fmt(drawing_mm)} mm</strong>
<CopyButton text={fmt2(result)} /> <CopyButton text={fmt(drawing_mm)} />
</p> </p>
{!isNaN(result) && ( {!isNaN(drawing_mm) && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
= {fmt2(result / 10)} cm pe desen = {fmt(drawing_mm / 10)} cm pe desen
</p> </p>
)} )}
</> </>
) : ( ) : (
<> <>
<p> <p>
Pe desen: <strong>{fmt2(val)} mm</strong> Pe desen: <strong>{fmt(val)} mm</strong>
</p> </p>
<p className="text-base border-t pt-2"> <p className="text-base border-t pt-2">
Real:{" "} Real:{" "}
<strong className="text-primary">{fmt2(result)} cm</strong> <strong className="text-primary">{fmt(real_in_unit)} {realUnit}</strong>
<CopyButton text={fmt2(result)} /> <CopyButton text={fmt(real_in_unit)} />
</p> </p>
{!isNaN(result) && ( {!isNaN(real_mm) && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
= {fmt2(result / 100)} m &nbsp;·&nbsp; {fmt2(result * 10)} mm {secondaryReal(real_mm)}
</p> </p>
)} )}
</> </>