ink-signature: pen-plotter pipeline for original wet-ink CMS signatures
The Standard no-login CMS path needs an ORIGINAL ink signature on paper (CMS-10114: 'Stamped, faxed or copied signatures will not be accepted'). This adds a pipeline to redraw the provider's own captured strokes in real ink with a pen on a CR-10 V2 (or any Marlin/GRBL machine) — original, in ink, never copied. - migration 090: esign_records.signature_vector (JSONB stroke paths, 0..1). - signing page now captures normalized stroke paths alongside the PNG; API stores a size-bounded vector for drawn signatures. - ink_signature_plotter.py (hardware-independent): fit strokes to the signature anchor box, PDF-pt -> bed-mm via jig offset, emit Marlin/GRBL G-code (Z pen or M280 servo/BLTouch), SVG toolpath preview, and render_signature_on_pdf (a digital twin that proves the toolpath lands on the cert line). Gated serial sender (dry_run default). - ink_signature_cli.py: end-to-end load-record -> gcode+preview, --test-box jig calibration, --plot to stream over USB. - Corrected CMS-10114 signature anchor to sit inside the Section 4A signing cell (above the bottom rule, below the label). - docs/ink-signature-plotter.md documents the CR-10 retrofit + interpretive risk. Tests: test_ink_signature.py 30/30, test_cms10114.py 27/27, test_paper_batch.py 15/15, API tsc clean, Astro build 58 pages.
This commit is contained in:
parent
e6a630ada1
commit
b0a8563a93
8 changed files with 994 additions and 19 deletions
|
|
@ -215,8 +215,17 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
var drawing = false;
|
||||
var hasSig = false;
|
||||
|
||||
// Vector capture: stroke paths normalized to the capture box (0..1, origin
|
||||
// top-left), resolution-independent. Drives the pen-plotter ink pipeline.
|
||||
var sigStrokes = []; // array of strokes; each stroke = array of {x,y,t}
|
||||
var curStroke = null;
|
||||
var strokeStart = 0;
|
||||
var captureW = 0, captureH = 0;
|
||||
|
||||
function resizeCanvas() {
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
captureW = rect.width;
|
||||
captureH = rect.height;
|
||||
canvas.width = rect.width * 2;
|
||||
canvas.height = rect.height * 2;
|
||||
ctx.scale(2, 2);
|
||||
|
|
@ -234,17 +243,51 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
return { x: t.clientX - rect.left, y: t.clientY - rect.top };
|
||||
}
|
||||
|
||||
canvas.addEventListener("mousedown", function(e) { drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); });
|
||||
canvas.addEventListener("mousemove", function(e) { if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSig = true; canvas.classList.add("has-sig"); document.getElementById("sig-hint").textContent = "Signature captured"; updateSubmit(); });
|
||||
canvas.addEventListener("mouseup", function() { drawing = false; });
|
||||
canvas.addEventListener("mouseleave", function() { drawing = false; });
|
||||
canvas.addEventListener("touchstart", function(e) { e.preventDefault(); drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); }, {passive:false});
|
||||
canvas.addEventListener("touchmove", function(e) { e.preventDefault(); if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSig = true; canvas.classList.add("has-sig"); document.getElementById("sig-hint").textContent = "Signature captured"; updateSubmit(); }, {passive:false});
|
||||
canvas.addEventListener("touchend", function() { drawing = false; });
|
||||
function beginStroke(p) {
|
||||
drawing = true;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
curStroke = [];
|
||||
strokeStart = Date.now();
|
||||
pushPoint(p);
|
||||
}
|
||||
|
||||
function pushPoint(p) {
|
||||
if (!curStroke) return;
|
||||
var w = captureW || 1, h = captureH || 1;
|
||||
curStroke.push({
|
||||
x: Math.max(0, Math.min(1, p.x / w)),
|
||||
y: Math.max(0, Math.min(1, p.y / h)),
|
||||
t: Date.now() - strokeStart,
|
||||
});
|
||||
}
|
||||
|
||||
function endStroke() {
|
||||
drawing = false;
|
||||
if (curStroke && curStroke.length) sigStrokes.push(curStroke);
|
||||
curStroke = null;
|
||||
}
|
||||
|
||||
function markSigged() {
|
||||
hasSig = true;
|
||||
canvas.classList.add("has-sig");
|
||||
document.getElementById("sig-hint").textContent = "Signature captured";
|
||||
updateSubmit();
|
||||
}
|
||||
|
||||
canvas.addEventListener("mousedown", function(e) { beginStroke(getPos(e)); });
|
||||
canvas.addEventListener("mousemove", function(e) { if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); pushPoint(p); markSigged(); });
|
||||
canvas.addEventListener("mouseup", endStroke);
|
||||
canvas.addEventListener("mouseleave", endStroke);
|
||||
canvas.addEventListener("touchstart", function(e) { e.preventDefault(); beginStroke(getPos(e)); }, {passive:false});
|
||||
canvas.addEventListener("touchmove", function(e) { e.preventDefault(); if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); pushPoint(p); markSigged(); }, {passive:false});
|
||||
canvas.addEventListener("touchend", endStroke);
|
||||
|
||||
document.getElementById("sig-clear").addEventListener("click", function() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
hasSig = false;
|
||||
sigStrokes = [];
|
||||
curStroke = null;
|
||||
canvas.classList.remove("has-sig");
|
||||
document.getElementById("sig-hint").textContent = "Draw your signature above";
|
||||
updateSubmit();
|
||||
|
|
@ -290,6 +333,11 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
var signatureData;
|
||||
if (sigMode === "draw") {
|
||||
signatureData = { type: "drawn", image_b64: canvas.toDataURL("image/png") };
|
||||
// Attach the vector strokes (resolution-independent) so the same signing
|
||||
// event can also drive the pen-plotter ink-signature pipeline.
|
||||
if (sigStrokes.length) {
|
||||
signatureData.vector = { v: 1, w: captureW, h: captureH, strokes: sigStrokes };
|
||||
}
|
||||
} else {
|
||||
signatureData = { type: "typed", name: typedInput.value.trim() };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue