Skip to content

Commit 0bdc9a4

Browse files
committed
adding improved debug visualization + save/load canvas config
1 parent 09246ff commit 0bdc9a4

1 file changed

Lines changed: 135 additions & 20 deletions

File tree

client-suede/robots.html

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,20 @@ <h2>Configure canvas</h2>
535535
</button>
536536
</div>
537537

538+
<div class="btn-row">
539+
<button class="ghost" onclick="exportConfig()">Export config</button>
540+
<button class="ghost" onclick="document.getElementById('importFile').click()">
541+
Import config
542+
</button>
543+
<input
544+
type="file"
545+
id="importFile"
546+
accept="application/json,.json"
547+
style="display: none"
548+
onchange="importConfig(this)"
549+
/>
550+
</div>
551+
538552
<div class="btn-row">
539553
<button class="gold" onclick="saveCanvas()">Save canvas</button>
540554
<button class="danger" onclick="clearDrawings()">Clear drawings</button>
@@ -845,6 +859,11 @@ <h2>Enqueue test drawing</h2>
845859
});
846860
// bots that live on this canvas (matched by region id)
847861
const regionIds = new Set((c.regions || []).map((r) => r.id));
862+
// Robot body geometry, all measured in mm from the pen (= reported pose).
863+
const NOSE_AHEAD = 76; // nose is 76mm in front of the pen
864+
const CAM_AHEAD = 51; // camera sits between pen and nose
865+
const TAIL_BEHIND = 226 - NOSE_AHEAD; // total length 226mm → 150mm behind pen
866+
const HALF_WHEEL = 149 / 2; // wheel axle spans 149mm across the pen
848867
(bots || [])
849868
.filter((b) => b.region && regionIds.has(b.region))
850869
.forEach((b) => {
@@ -856,20 +875,40 @@ <h2>Enqueue test drawing</h2>
856875
: b.status === "ready"
857876
? "#2d7d46"
858877
: "#c79a2e";
859-
// Arrow for heading
860878
const rad = (b.pose.headingDegrees * Math.PI) / 180;
861-
const arrowLen = fs * 1.4;
862-
const ax = b.pose.x + Math.cos(rad) * arrowLen;
863-
const ay = b.pose.y + Math.sin(rad) * arrowLen;
879+
const fx = Math.cos(rad), fy = Math.sin(rad); // forward (heading) unit
880+
const rx = -Math.sin(rad), ry = Math.cos(rad); // rightward unit
881+
const pen = { x: b.pose.x, y: b.pose.y };
882+
const along = (d) => ({ x: pen.x + fx * d, y: pen.y + fy * d });
883+
const nose = along(NOSE_AHEAD);
884+
const tail = along(-TAIL_BEHIND);
885+
const cam = along(CAM_AHEAD);
886+
const wheelL = { x: pen.x + rx * HALF_WHEEL, y: pen.y + ry * HALF_WHEEL };
887+
const wheelR = { x: pen.x - rx * HALF_WHEEL, y: pen.y - ry * HALF_WHEEL };
888+
889+
// Spine (tail → nose) and wheel axle (across the pen) — the "t".
890+
parts.push(
891+
`<line x1="${tail.x}" y1="${tail.y}" x2="${nose.x}" y2="${nose.y}" stroke="${color}" stroke-width="${sw * 1.5}" stroke-linecap="round"/>`,
892+
);
893+
parts.push(
894+
`<line x1="${wheelL.x}" y1="${wheelL.y}" x2="${wheelR.x}" y2="${wheelR.y}" stroke="${color}" stroke-width="${sw * 1.5}" stroke-linecap="round"/>`,
895+
);
864896

897+
// Camera as a triangle pointing along the heading.
898+
const TRI = 34; // mm
899+
const apex = { x: cam.x + fx * TRI * 0.6, y: cam.y + fy * TRI * 0.6 };
900+
const cL = { x: cam.x - fx * TRI * 0.4 + rx * TRI * 0.5, y: cam.y - fy * TRI * 0.4 + ry * TRI * 0.5 };
901+
const cR = { x: cam.x - fx * TRI * 0.4 - rx * TRI * 0.5, y: cam.y - fy * TRI * 0.4 - ry * TRI * 0.5 };
865902
parts.push(
866-
`<line x1="${b.pose.x}" y1="${b.pose.y}" x2="${ax}" y2="${ay}" stroke="${color}" stroke-width="${sw * 1.5}" marker-end="url(#arrowhead)"/>`,
903+
`<polygon points="${apex.x},${apex.y} ${cL.x},${cL.y} ${cR.x},${cR.y}" fill="${color}" stroke="#fff" stroke-width="${sw * 0.5}"/>`,
867904
);
905+
906+
// Pen location (the reported pose) as a small dot.
868907
parts.push(
869-
`<circle cx="${b.pose.x}" cy="${b.pose.y}" r="${fs * 0.7}" fill="${color}" stroke="#fff" stroke-width="${sw}"/>`,
908+
`<circle cx="${pen.x}" cy="${pen.y}" r="${sw * 2.2}" fill="#fff" stroke="${color}" stroke-width="${sw}"/>`,
870909
);
871910
parts.push(
872-
`<text x="${b.pose.x + fs * 0.9}" y="${b.pose.y + fs * 0.4}" font-size="${fs * 0.8}" font-family="monospace" fill="#222">${escSvg(b.name)}</text>`,
911+
`<text x="${pen.x + fs * 0.9}" y="${pen.y + fs * 0.4}" font-size="${fs * 0.8}" font-family="monospace" fill="#222">${escSvg(b.name)}</text>`,
873912
);
874913
});
875914
parts.push("</svg>");
@@ -961,26 +1000,102 @@ <h2>Enqueue test drawing</h2>
9611000
editorToast(`Generated ${rows * cols} regions`, true);
9621001
}
9631002

964-
/** Populate the editor from a canvas returned by the server. */
965-
function loadSelected() {
966-
const id = document.getElementById("loadSelect").value;
967-
const c = (window._canvases || []).find((x) => x.id === id);
968-
if (!c) return;
969-
document.getElementById("cId").value = c.id;
970-
document.getElementById("cW").value = c.width;
971-
document.getElementById("cH").value = c.height;
1003+
/**
1004+
* Populate the whole editor from a canvas config object. Accepts both the
1005+
* server shape (from GET /api/robots/canvases, no `placement`) and the
1006+
* exported payload shape (from buildCanvasPayload, with `placement`).
1007+
* Placement fields are only touched when the config carries them, so
1008+
* loading a server canvas leaves the current placement settings intact.
1009+
* @param {any} cfg
1010+
*/
1011+
function applyCanvasConfig(cfg) {
1012+
if (cfg.id != null) document.getElementById("cId").value = cfg.id;
1013+
if (cfg.width != null) document.getElementById("cW").value = cfg.width;
1014+
if (cfg.height != null) document.getElementById("cH").value = cfg.height;
1015+
1016+
const p = cfg.placement;
1017+
if (p) {
1018+
const set = (id, v) => {
1019+
if (v != null) document.getElementById(id).value = v;
1020+
};
1021+
set("pCell", p.cellMm);
1022+
set("pPen", p.penMm);
1023+
set("pClear", p.clearanceMm);
1024+
set("pStep", p.searchStepCells);
1025+
set("pAngle", p.angleStepDeg);
1026+
if (p.strategy != null)
1027+
document.getElementById("pStrategy").value = p.strategy;
1028+
set("pTargetFootprint", p.targetFootprintMm);
1029+
set("pMinScale", p.minFootprintScale);
1030+
}
1031+
9721032
document.getElementById("markersList").innerHTML = "";
973-
(c.markers || []).forEach((m) =>
1033+
(cfg.markers || []).forEach((m) =>
9741034
addMarkerRow({
9751035
id: m.id,
976-
x: m.position.x,
977-
y: m.position.y,
1036+
x: m.position ? m.position.x : m.x,
1037+
y: m.position ? m.position.y : m.y,
9781038
sizeMm: m.sizeMm,
9791039
}),
9801040
);
9811041
document.getElementById("regionsList").innerHTML = "";
982-
(c.regions || []).forEach((r) => addRegionRow(r));
983-
editorToast(`Loaded "${c.id}" (placement defaults apply)`, true);
1042+
(cfg.regions || []).forEach((r) => addRegionRow(r));
1043+
}
1044+
1045+
/** Populate the editor from a canvas returned by the server. */
1046+
function loadSelected() {
1047+
const id = document.getElementById("loadSelect").value;
1048+
const c = (window._canvases || []).find((x) => x.id === id);
1049+
if (!c) return;
1050+
applyCanvasConfig(c);
1051+
editorToast(
1052+
`Loaded "${c.id}"${c.placement ? "" : " (placement defaults apply)"}`,
1053+
true,
1054+
);
1055+
}
1056+
1057+
/** Download the current editor state as a complete JSON config file. */
1058+
function exportConfig() {
1059+
const payload = buildCanvasPayload();
1060+
if (!payload.id) return editorToast("Canvas id is required", false);
1061+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
1062+
type: "application/json",
1063+
});
1064+
const url = URL.createObjectURL(blob);
1065+
const a = document.createElement("a");
1066+
a.href = url;
1067+
a.download = `${payload.id}.canvas.json`;
1068+
document.body.appendChild(a);
1069+
a.click();
1070+
a.remove();
1071+
URL.revokeObjectURL(url);
1072+
editorToast(`Exported "${payload.id}"`, true);
1073+
}
1074+
1075+
/**
1076+
* Load a complete config from a chosen JSON file into the editor. Does not
1077+
* save — the user reviews the populated fields and clicks Save canvas.
1078+
* @param {HTMLInputElement} input
1079+
*/
1080+
function importConfig(input) {
1081+
const file = input.files && input.files[0];
1082+
if (!file) return;
1083+
const reader = new FileReader();
1084+
reader.onload = () => {
1085+
try {
1086+
const cfg = JSON.parse(/** @type {string} */ (reader.result));
1087+
applyCanvasConfig(cfg);
1088+
editorToast(
1089+
`Imported "${cfg.id || file.name}" — review, then Save canvas`,
1090+
true,
1091+
);
1092+
} catch (e) {
1093+
editorToast("Import failed: " + e.message, false);
1094+
}
1095+
};
1096+
reader.onerror = () => editorToast("Could not read file", false);
1097+
reader.readAsText(file);
1098+
input.value = ""; // let the same file be re-imported later
9841099
}
9851100

9861101
/** @returns {object} CanvasConfig payload for POST /api/robots/canvases */

0 commit comments

Comments
 (0)