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:
@@ -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 · {fmt2(result * 10)} mm
|
{secondaryReal(real_mm)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user