Add rulers and cm measurements to personalization canvas

- Display X and Y rulers with 2cm increments
- Show position and size in centimeters (cm) instead of pixels
- Calculate cmPerPx from product print area for accurate conversion
- Rulers positioned above and to the left of design canvas
- Labels show cm measurements for easy reference while personalizing

Users can now see exact placement and dimensions in cm during design,
with visual ruler guides for precise positioning.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-18 11:51:07 +01:00
co-authored by Claude Haiku 4.5
parent 209af186b3
commit c60491deca
+81 -5
View File
@@ -200,6 +200,77 @@ function PrintSurface({
<div className="pointer-events-none absolute inset-0" style={{ width: DISPLAY, height: DISPLAY }}> <div className="pointer-events-none absolute inset-0" style={{ width: DISPLAY, height: DISPLAY }}>
{photo} {photo}
</div> </div>
{/* Horizontal Ruler */}
<div
className="absolute pointer-events-none bg-paper border-b border-line text-xs font-mono"
style={{
left: stageLeft,
top: stageTop - 20,
width: stageW,
height: 18,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: '2px',
}}
>
{Array.from({ length: Math.ceil((stageW / (10 * (product.printAreaWidthMm / 100)))) }).map((_, i) => {
const cm = i * 2;
const px = (cm * stageW) / (product.printAreaWidthMm / 10);
if (px > stageW) return null;
return (
<div
key={`h-${cm}`}
style={{
position: 'absolute',
left: `${px}px`,
fontSize: '9px',
color: '#888',
transform: 'translateX(-50%)',
}}
>
{cm}cm
</div>
);
})}
</div>
{/* Vertical Ruler */}
<div
className="absolute pointer-events-none bg-paper border-r border-line text-xs font-mono"
style={{
left: stageLeft - 28,
top: stageTop,
width: 26,
height: stageH,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
paddingRight: '4px',
}}
>
{Array.from({ length: Math.ceil((stageH / (10 * (product.printAreaWidthMm / 100)))) }).map((_, i) => {
const cm = i * 2;
const px = (cm * stageH) / (product.printAreaWidthMm / 10);
if (px > stageH) return null;
return (
<div
key={`v-${cm}`}
style={{
position: 'absolute',
top: `${px}px`,
fontSize: '9px',
color: '#888',
transform: 'translateY(-50%)',
}}
>
{cm}
</div>
);
})}
</div>
<div className="absolute" style={{ left: stageLeft, top: stageTop, width: stageW, height: stageH }}> <div className="absolute" style={{ left: stageLeft, top: stageTop, width: stageW, height: stageH }}>
<Stage <Stage
ref={stageRef} ref={stageRef}
@@ -348,6 +419,11 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
const activePrintArea = view === 'front' ? frontPrintArea : backPrintArea; const activePrintArea = view === 'front' ? frontPrintArea : backPrintArea;
const activeStageW = activePrintArea.wPct * DISPLAY; const activeStageW = activePrintArea.wPct * DISPLAY;
const activeStageH = activePrintArea.hPct * DISPLAY; const activeStageH = activePrintArea.hPct * DISPLAY;
// Calculate cm per pixel for display
const printAreaWidthPx = activePrintArea.wPct * DISPLAY;
const cmPerPx = (product.printAreaWidthMm / printAreaWidthPx) / 10;
const pxToCm = (px: number) => (px * cmPerPx).toFixed(2);
const setActiveElements = view === 'front' ? setElementsFront : setElementsBack; const setActiveElements = view === 'front' ? setElementsFront : setElementsBack;
// Keep whichever transformer actually owns the selected element in sync, and // Keep whichever transformer actually owns the selected element in sync, and
@@ -779,25 +855,25 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
{selected && ( {selected && (
<div className="mt-4 border border-line bg-surface p-4"> <div className="mt-4 border border-line bg-surface p-4">
<p className="mb-3 font-medium text-sm">Position & Size</p> <p className="mb-3 font-medium text-sm">Position & Size (cm)</p>
<div className="space-y-2 text-xs font-mono"> <div className="space-y-2 text-xs font-mono">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted">X:</span> <span className="text-muted">X:</span>
<span>{Math.round(selected.x)} px</span> <span>{pxToCm(selected.x)} cm</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted">Y:</span> <span className="text-muted">Y:</span>
<span>{Math.round(selected.y)} px</span> <span>{pxToCm(selected.y)} cm</span>
</div> </div>
{selected.type === 'image' && ( {selected.type === 'image' && (
<> <>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted">Width:</span> <span className="text-muted">Width:</span>
<span>{Math.round(selected.width)} px</span> <span>{pxToCm(selected.width)} cm</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted">Height:</span> <span className="text-muted">Height:</span>
<span>{Math.round(selected.height)} px</span> <span>{pxToCm(selected.height)} cm</span>
</div> </div>
</> </>
)} )}