feat(layout): add grid, safe zone, and MICR anchor alignment
- Draw 1/8" grid overlay on layout editor canvas - Anchor MICR second transit symbol at 2 59/64" from left - Clamp draggable fields to printing safe zone (11/64" sides, 13/64" top, 0.5" bottom) - Render dashed safe-zone outline on layout canvas
This commit is contained in:
+44
-8
@@ -2082,6 +2082,12 @@ function populateLayoutDropdown() {
|
|||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printing safe zone for user-adjustable fields (inches). MICR is exempt.
|
||||||
|
const SAFE_LEFT = 11 / 64;
|
||||||
|
const SAFE_RIGHT = 8.5 - 11 / 64;
|
||||||
|
const SAFE_TOP = 13 / 64;
|
||||||
|
const SAFE_BOTTOM = 3.5 - 0.5;
|
||||||
|
|
||||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
function svgEl(tag, attrs, text) {
|
function svgEl(tag, attrs, text) {
|
||||||
const el = document.createElementNS(SVG_NS, tag);
|
const el = document.createElementNS(SVG_NS, tag);
|
||||||
@@ -2104,11 +2110,41 @@ function renderLayoutCanvas() {
|
|||||||
// White check background
|
// White check background
|
||||||
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
||||||
|
|
||||||
|
// Grid at 1/8" increments (darker every 1/4", darkest on whole inches)
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(8.5 * 8); n8++) {
|
||||||
|
const x = (n8 / 8) * SCALE;
|
||||||
|
if (x >= W) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:x, y1:0, x2:x, y2:H, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(3.5 * 8); n8++) {
|
||||||
|
const y = (n8 / 8) * SCALE;
|
||||||
|
if (y >= H) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:0, y1:y, x2:W, y2:y, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
|
||||||
// MICR reference line
|
// MICR reference line
|
||||||
const micrY = (3.5 - 0.267) * SCALE;
|
const micrY = (3.5 - 0.267) * SCALE;
|
||||||
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
||||||
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
||||||
|
|
||||||
|
// Safe zone outline for user-adjustable fields
|
||||||
|
svg.appendChild(svgEl('rect', {
|
||||||
|
x: SAFE_LEFT * SCALE,
|
||||||
|
y: SAFE_TOP * SCALE,
|
||||||
|
width: (SAFE_RIGHT - SAFE_LEFT) * SCALE,
|
||||||
|
height: (SAFE_BOTTOM - SAFE_TOP) * SCALE,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#60a5fa',
|
||||||
|
'stroke-width': 1,
|
||||||
|
'stroke-dasharray': '3,3',
|
||||||
|
}));
|
||||||
|
|
||||||
for (const f of layoutState.fields) {
|
for (const f of layoutState.fields) {
|
||||||
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
||||||
svg.appendChild(g);
|
svg.appendChild(g);
|
||||||
@@ -2333,11 +2369,11 @@ function onLayoutDragMove(e) {
|
|||||||
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
||||||
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
f.x_pos = clampIn(round16(layoutDrag.origX + dx), 0, 8.5);
|
f.x_pos = clampIn(round16(layoutDrag.origX + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(layoutDrag.origY + dy), 0, 3.5);
|
f.y_pos = clampIn(round16(layoutDrag.origY + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (layoutDrag.moveEnd) {
|
if (layoutDrag.moveEnd) {
|
||||||
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), 0, 8.5);
|
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), 0, 3.5);
|
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
// Update just the dragged element for smooth performance
|
// Update just the dragged element for smooth performance
|
||||||
const svg = document.querySelector('#layout-canvas-container svg');
|
const svg = document.querySelector('#layout-canvas-container svg');
|
||||||
@@ -2364,11 +2400,11 @@ function nudgeLayoutField(dx, dy) {
|
|||||||
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const S = 1 / 16;
|
const S = 1 / 16;
|
||||||
f.x_pos = clampIn(round16(f.x_pos + dx * S), 0, 8.5);
|
f.x_pos = clampIn(round16(f.x_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(f.y_pos + dy * S), 0, 3.5);
|
f.y_pos = clampIn(round16(f.y_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
||||||
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), 0, 8.5);
|
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), 0, 3.5);
|
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
updateLayoutSidebar(f);
|
updateLayoutSidebar(f);
|
||||||
renderLayoutCanvas();
|
renderLayoutCanvas();
|
||||||
|
|||||||
@@ -235,17 +235,27 @@ function generateCheckPdf(account, checks, fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- MICR line ---
|
// --- MICR line ---
|
||||||
|
// Anchor the second transit symbol (the 'A' after routing) at 2 59/64" from left.
|
||||||
|
// Routing extends left from that anchor; account + check extend right.
|
||||||
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
||||||
const micrPos = pt(0.3, MICR_Y_IN);
|
const ANCHOR_IN = 2 + 59 / 64;
|
||||||
|
|
||||||
if (hasMicrFont) {
|
if (hasMicrFont) {
|
||||||
doc.font('MICR').fontSize(12).fillColor('#000000')
|
doc.font('MICR').fontSize(12).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
} else {
|
} else {
|
||||||
doc.font('Courier').fontSize(10).fillColor('#000000')
|
doc.font('Courier').fontSize(10).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix = everything up to and including the second 'A' (first A + routing + second A).
|
||||||
|
const secondA = micrLine.indexOf('A', 1) + 1;
|
||||||
|
const prefix = micrLine.slice(0, secondA);
|
||||||
|
const prefixWidthPts = doc.widthOfString(prefix);
|
||||||
|
const anchorXPts = (ANCHOR_IN + offX) * POINTS_PER_INCH;
|
||||||
|
const micrXPts = anchorXPts - prefixWidthPts;
|
||||||
|
const micrYPts = (slotOriginY + MICR_Y_IN + offY) * POINTS_PER_INCH;
|
||||||
|
|
||||||
|
doc.text(micrLine, micrXPts, micrYPts, { lineBreak: false });
|
||||||
|
|
||||||
} // end slot loop
|
} // end slot loop
|
||||||
} // end page loop
|
} // end page loop
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user