Fix measurement display and ruler rendering

- Add printAreaWidthMm and printAreaHeightMm to ProductDTO type
- Include these fields in toProductDTO conversion function
- Refactor ruler calculations to use simple for loops
- Rulers now display correctly showing 2cm increments on both axes
- Position & Size panel now shows accurate cm measurements

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-18 12:08:14 +01:00
co-authored by Claude Haiku 4.5
parent 4658b5a8b8
commit c20c696ccb
3 changed files with 70 additions and 42 deletions
+41 -17
View File
@@ -204,6 +204,7 @@ function PrintSurface({
</div> </div>
{/* Horizontal Ruler */} {/* Horizontal Ruler */}
{product.printAreaWidthMm > 0 && stageW > 0 && (
<div <div
className="absolute pointer-events-none bg-paper border-b border-line text-xs font-mono" className="absolute pointer-events-none bg-paper border-b border-line text-xs font-mono"
style={{ style={{
@@ -212,16 +213,19 @@ function PrintSurface({
width: stageW, width: stageW,
height: 18, height: 18,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'flex-end',
justifyContent: 'space-between',
paddingBottom: '2px', paddingBottom: '2px',
}} }}
> >
{Array.from({ length: Math.ceil((stageW / (10 * (product.printAreaWidthMm / 100)))) }).map((_, i) => { {(() => {
const cm = i * 2; const marks = [];
const px = (cm * stageW) / (product.printAreaWidthMm / 10); const cmPerStageW = product.printAreaWidthMm / 10; // total cm width of stage
if (px > stageW) return null; const pxPerCm = stageW / cmPerStageW;
return (
for (let cm = 0; cm <= Math.ceil(cmPerStageW); cm += 2) {
const px = cm * pxPerCm;
if (px > stageW) break;
marks.push(
<div <div
key={`h-${cm}`} key={`h-${cm}`}
style={{ style={{
@@ -230,15 +234,20 @@ function PrintSurface({
fontSize: '9px', fontSize: '9px',
color: '#888', color: '#888',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
whiteSpace: 'nowrap',
}} }}
> >
{cm}cm {cm}
</div> </div>
); );
})} }
return marks;
})()}
</div> </div>
)}
{/* Vertical Ruler */} {/* Vertical Ruler */}
{product.printAreaHeightMm > 0 && stageH > 0 && (
<div <div
className="absolute pointer-events-none bg-paper border-r border-line text-xs font-mono" className="absolute pointer-events-none bg-paper border-r border-line text-xs font-mono"
style={{ style={{
@@ -252,11 +261,15 @@ function PrintSurface({
paddingRight: '4px', paddingRight: '4px',
}} }}
> >
{Array.from({ length: Math.ceil((stageH / (10 * (product.printAreaWidthMm / 100)))) }).map((_, i) => { {(() => {
const cm = i * 2; const marks = [];
const px = (cm * stageH) / (product.printAreaWidthMm / 10); const cmPerStageH = product.printAreaHeightMm / 10; // total cm height of stage
if (px > stageH) return null; const pxPerCm = stageH / cmPerStageH;
return (
for (let cm = 0; cm <= Math.ceil(cmPerStageH); cm += 2) {
const px = cm * pxPerCm;
if (px > stageH) break;
marks.push(
<div <div
key={`v-${cm}`} key={`v-${cm}`}
style={{ style={{
@@ -265,13 +278,17 @@ function PrintSurface({
fontSize: '9px', fontSize: '9px',
color: '#888', color: '#888',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
whiteSpace: 'nowrap',
}} }}
> >
{cm} {cm}
</div> </div>
); );
})} }
return marks;
})()}
</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
@@ -424,8 +441,15 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
// Calculate cm per pixel for display // Calculate cm per pixel for display
const printAreaWidthPx = activePrintArea.wPct * DISPLAY; const printAreaWidthPx = activePrintArea.wPct * DISPLAY;
const cmPerPx = (product.printAreaWidthMm / printAreaWidthPx) / 10; // printAreaWidthMm is in mm, divide by 10 to get cm
const pxToCm = (px: number) => (px * cmPerPx).toFixed(2); // printAreaWidthPx is in pixels
// cmPerPx = cm per pixel
const mmPerPx = printAreaWidthPx > 0 ? product.printAreaWidthMm / printAreaWidthPx : 0;
const cmPerPx = mmPerPx > 0 ? mmPerPx / 10 : 0;
const pxToCm = (px: number) => {
const cm = px * cmPerPx;
return isNaN(cm) ? '?' : cm.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
+2
View File
@@ -19,6 +19,8 @@ export function toProductDTO(p: PrismaProduct, activePromotions: ActivePromotion
mockup: p.mockup, mockup: p.mockup,
printArea: JSON.parse(p.printArea), printArea: JSON.parse(p.printArea),
printAreaBack: p.printAreaBack ? JSON.parse(p.printAreaBack) : null, printAreaBack: p.printAreaBack ? JSON.parse(p.printAreaBack) : null,
printAreaWidthMm: p.printAreaWidthMm,
printAreaHeightMm: p.printAreaHeightMm,
imageUrl: p.imageUrl, imageUrl: p.imageUrl,
imageUrlBack: p.imageUrlBack, imageUrlBack: p.imageUrlBack,
photoRecolorable: p.photoRecolorable, photoRecolorable: p.photoRecolorable,
+2
View File
@@ -43,6 +43,8 @@ export type ProductDTO = {
mockup: string; mockup: string;
printArea: PrintArea; printArea: PrintArea;
printAreaBack: PrintArea | null; printAreaBack: PrintArea | null;
printAreaWidthMm: number;
printAreaHeightMm: number;
imageUrl: string | null; imageUrl: string | null;
imageUrlBack: string | null; imageUrlBack: string | null;
photoRecolorable: boolean; photoRecolorable: boolean;