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 },
|
||||
];
|
||||
|
||||
// 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,7 +2623,8 @@ function ScaleCalculator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode */}
|
||||
{/* 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"}
|
||||
@@ -2618,26 +2641,46 @@ function ScaleCalculator() {
|
||||
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)
|
||||
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 · {fmt2(result * 10)} mm
|
||||
{secondaryReal(real_mm)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user