rotation vorbereitung
This commit is contained in:
@@ -159,7 +159,9 @@
|
|||||||
<span><span class="dot circle" style="background:#dde3ec"></span>Erkannt (nur 2D)</span>
|
<span><span class="dot circle" style="background:#dde3ec"></span>Erkannt (nur 2D)</span>
|
||||||
<span><span class="dot circle" style="background:#fbbf24"></span>Gemessen (3b)</span>
|
<span><span class="dot circle" style="background:#fbbf24"></span>Gemessen (3b)</span>
|
||||||
<span><span class="dot circle" style="background:#3b82f6"></span>Fremd (3b)</span>
|
<span><span class="dot circle" style="background:#3b82f6"></span>Fremd (3b)</span>
|
||||||
<span><span class="dot circle" style="background:#f97316"></span>Vergleich</span>
|
<span><span class="dot circle" style="background:#f97316"></span>Pos B</span>
|
||||||
|
<span><span class="dot circle" style="background:#22d3ee"></span>Pos C</span>
|
||||||
|
<span><span class="dot circle" style="background:#fb7185"></span>Kreismittelpkt.</span>
|
||||||
<span><span class="dot" style="background:#9b7bff"></span>Kamera</span>
|
<span><span class="dot" style="background:#9b7bff"></span>Kamera</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="stats"></span>
|
<span id="stats"></span>
|
||||||
@@ -172,12 +174,18 @@
|
|||||||
<select id="sel-run-primary" class="btn" style="min-width:158px">
|
<select id="sel-run-primary" class="btn" style="min-width:158px">
|
||||||
<option value="">⟳ aktuellster</option>
|
<option value="">⟳ aktuellster</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="run-lbl" style="margin-left:10px">Vergleich
|
<span class="run-lbl" style="margin-left:10px">Pos B
|
||||||
<span style="text-transform:none;opacity:.7;font-size:9px">(nur fremd)</span>
|
<span style="text-transform:none;opacity:.7;font-size:9px">(nur fremd)</span>
|
||||||
</span>
|
</span>
|
||||||
<select id="sel-run-compare" class="btn" style="min-width:158px">
|
<select id="sel-run-compare" class="btn" style="min-width:158px">
|
||||||
<option value="">– keiner –</option>
|
<option value="">– keiner –</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span class="run-lbl" style="margin-left:10px">Pos C
|
||||||
|
<span style="text-transform:none;opacity:.7;font-size:9px">(Y-Achse)</span>
|
||||||
|
</span>
|
||||||
|
<select id="sel-run-c" class="btn" style="min-width:158px">
|
||||||
|
<option value="">– keiner –</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="canvas-wrap">
|
<div id="canvas-wrap">
|
||||||
@@ -229,13 +237,16 @@ const gPaper = new THREE.Group(); // weißes A0-Papier
|
|||||||
const gMarkers = new THREE.Group(); // Modell-Rechtecke
|
const gMarkers = new THREE.Group(); // Modell-Rechtecke
|
||||||
const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
|
const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
|
||||||
const gCameras = new THREE.Group(); // Kamera-Frusta
|
const gCameras = new THREE.Group(); // Kamera-Frusta
|
||||||
const gCompare = new THREE.Group(); // Vergleichs-Punkte (anderer Timestamp, nur fremd)
|
const gCompare = new THREE.Group(); // Pos B Marker (nur fremd, orange)
|
||||||
const gCompareLines = new THREE.Group(); // Verbindungslinien Basis↔Vergleich
|
const gCompareLines = new THREE.Group(); // Verbindungslinien Pos A↔Pos B
|
||||||
scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines);
|
const gPositionC = new THREE.Group(); // Pos C Marker (nur fremd, cyan)
|
||||||
|
const gYAxis = new THREE.Group(); // Y-Achse Visualisierung (Kreismittelpunkte, Achse)
|
||||||
|
scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines, gPositionC, gYAxis);
|
||||||
|
|
||||||
// ── Zustand für Vergleichs-Linien ─────────────────────────────────────────────
|
// ── Zustand für Positionen ────────────────────────────────────────────────────
|
||||||
let _primaryFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
|
let _primaryFremdMarkers = []; // Pos A – [{marker_id, position_mm, num_cameras}]
|
||||||
let _compareFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
|
let _compareFremdMarkers = []; // Pos B – [{marker_id, position_mm, num_cameras}]
|
||||||
|
let _positionCFremdMarkers = []; // Pos C – [{marker_id, position_mm, num_cameras}]
|
||||||
|
|
||||||
// ── Viewer-interner Logger ────────────────────────────────────────────────────
|
// ── Viewer-interner Logger ────────────────────────────────────────────────────
|
||||||
function vlog(msg, kind = '') {
|
function vlog(msg, kind = '') {
|
||||||
@@ -663,13 +674,19 @@ function buildCompareLines() {
|
|||||||
const noMatch = onlyPrimary.filter(id => !matchedIds.includes(id));
|
const noMatch = onlyPrimary.filter(id => !matchedIds.includes(id));
|
||||||
const parts = [`${matchedIds.length} Linien`];
|
const parts = [`${matchedIds.length} Linien`];
|
||||||
if (matchedIds.length) parts.push(`IDs: ${matchedIds.join(' ')}`);
|
if (matchedIds.length) parts.push(`IDs: ${matchedIds.join(' ')}`);
|
||||||
if (onlyCompare.length) parts.push(`nur Vergleich: ${onlyCompare.join(' ')}`);
|
if (onlyCompare.length) parts.push(`nur Pos B: ${onlyCompare.join(' ')}`);
|
||||||
if (noMatch.length) parts.push(`nur Basis: ${noMatch.join(' ')}`);
|
if (noMatch.length) parts.push(`nur Pos A: ${noMatch.join(' ')}`);
|
||||||
vlog(parts.join(' | '), matchedIds.length ? 'ok' : 'warn');
|
vlog(parts.join(' | '), matchedIds.length ? 'ok' : 'warn');
|
||||||
|
|
||||||
// ── Bewegungsanalyse: mittlerer Verschiebungsvektor → Abweichung von X-Achse ──
|
// ── Bewegungsanalyse ──────────────────────────────────────────────────────────
|
||||||
if (matchedIds.length > 0) {
|
// Pos C aktiv → Bogen-Modus (Y-Achse), keine lineare X-Achsen-Analyse
|
||||||
// Summe aller Verschiebungsvektoren (in Roboter-Koordinaten mm)
|
const posC_active = !!document.getElementById('sel-run-c')?.value;
|
||||||
|
|
||||||
|
if (posC_active) {
|
||||||
|
vlog('Pos C aktiv → Bogen-Modus, X-Achsen-Analyse übersprungen');
|
||||||
|
window.parent.postMessage({ type: 'xaxis-measurement', direction: null }, '*');
|
||||||
|
} else if (matchedIds.length > 0) {
|
||||||
|
// ── Lineare X-Achsen-Analyse (mittlerer Verschiebungsvektor) ──────────────
|
||||||
let sx = 0, sy = 0, sz = 0;
|
let sx = 0, sy = 0, sz = 0;
|
||||||
for (const cm of _compareFremdMarkers) {
|
for (const cm of _compareFremdMarkers) {
|
||||||
const pm = primaryMap.get(cm.marker_id);
|
const pm = primaryMap.get(cm.marker_id);
|
||||||
@@ -699,7 +716,6 @@ function buildCompareLines() {
|
|||||||
vlog(`Bewegung: ⌀${dist.toFixed(2)} mm dir=[${vx.toFixed(4)}, ${vy.toFixed(4)}, ${vz.toFixed(4)}] (${n} Marker)`);
|
vlog(`Bewegung: ⌀${dist.toFixed(2)} mm dir=[${vx.toFixed(4)}, ${vy.toFixed(4)}, ${vz.toFixed(4)}] (${n} Marker)`);
|
||||||
vlog(`Abw. von X-Achse: horizontal(XY) ${fmt(degXY)} vertikal(XZ) ${fmt(degXZ)}`, good ? 'ok' : 'warn');
|
vlog(`Abw. von X-Achse: horizontal(XY) ${fmt(degXY)} vertikal(XZ) ${fmt(degXZ)}`, good ? 'ok' : 'warn');
|
||||||
|
|
||||||
// Parent informieren → aktiviert "X-Achse übernehmen"-Button
|
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'xaxis-measurement',
|
type: 'xaxis-measurement',
|
||||||
direction: [vx, vy, vz],
|
direction: [vx, vy, vz],
|
||||||
@@ -710,10 +726,129 @@ function buildCompareLines() {
|
|||||||
}, '*');
|
}, '*');
|
||||||
} else {
|
} else {
|
||||||
vlog(`Bewegung zu klein (${dist.toFixed(3)} mm) – Winkelberechnung übersprungen`, 'warn');
|
vlog(`Bewegung zu klein (${dist.toFixed(3)} mm) – Winkelberechnung übersprungen`, 'warn');
|
||||||
// Messung ungültig → Parent mitteilen
|
|
||||||
window.parent.postMessage({ type: 'xaxis-measurement', direction: null }, '*');
|
window.parent.postMessage({ type: 'xaxis-measurement', direction: null }, '*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Y-Achse neu berechnen (nutzt _positionCFremdMarkers – kein-op wenn leer)
|
||||||
|
computeAndShowYAxis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Y-Achsen-Berechnung aus drei Positionen ───────────────────────────────────
|
||||||
|
|
||||||
|
function computeAndShowYAxis() {
|
||||||
|
clearGroup(gYAxis);
|
||||||
|
|
||||||
|
// Nur aktiv wenn alle drei Positionen vorliegen
|
||||||
|
if (_positionCFremdMarkers.length === 0) return;
|
||||||
|
if (_primaryFremdMarkers.length === 0) return;
|
||||||
|
if (_compareFremdMarkers.length === 0) {
|
||||||
|
vlog('Y-Achse: Pos B nicht gewählt – kein Vergleich aktiv', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapA = new Map(_primaryFremdMarkers .map(m => [m.marker_id, m]));
|
||||||
|
const mapB = new Map(_compareFremdMarkers .map(m => [m.marker_id, m]));
|
||||||
|
const mapC = new Map(_positionCFremdMarkers.map(m => [m.marker_id, m]));
|
||||||
|
|
||||||
|
function dist2(P, Q) { return (P[0]-Q[0])**2 + (P[1]-Q[1])**2 + (P[2]-Q[2])**2; }
|
||||||
|
|
||||||
|
const circumcenters = []; // [{id, C:[x,y,z]}] in mm
|
||||||
|
const normals = []; // [[nx,ny,nz]] – Achsenrichtung je Marker
|
||||||
|
|
||||||
|
for (const [id, ma] of mapA) {
|
||||||
|
const mb = mapB.get(id);
|
||||||
|
const mc = mapC.get(id);
|
||||||
|
if (!mb || !mc) continue;
|
||||||
|
|
||||||
|
const P1 = ma.position_mm.map(Number);
|
||||||
|
const P2 = mb.position_mm.map(Number);
|
||||||
|
const P3 = mc.position_mm.map(Number);
|
||||||
|
|
||||||
|
// Normalenvektor der Kreisebene = Achsenrichtung
|
||||||
|
const v1 = [P2[0]-P1[0], P2[1]-P1[1], P2[2]-P1[2]];
|
||||||
|
const v2 = [P3[0]-P1[0], P3[1]-P1[1], P3[2]-P1[2]];
|
||||||
|
const cross = [
|
||||||
|
v1[1]*v2[2] - v1[2]*v2[1],
|
||||||
|
v1[2]*v2[0] - v1[0]*v2[2],
|
||||||
|
v1[0]*v2[1] - v1[1]*v2[0],
|
||||||
|
];
|
||||||
|
const crossLen = Math.sqrt(cross[0]**2 + cross[1]**2 + cross[2]**2);
|
||||||
|
if (crossLen < 1e-3) {
|
||||||
|
vlog(`Y-Achse: Marker ${id} degenerat (Punkte zu nahe / kollinear)`, 'warn');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const n = cross.map(c => c / crossLen);
|
||||||
|
normals.push(n);
|
||||||
|
|
||||||
|
// Umkreismittelpunkt (baryzentrischer Ansatz)
|
||||||
|
const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
|
||||||
|
const w1 = a2*(b2+c2-a2), w2 = b2*(a2+c2-b2), w3 = c2*(a2+b2-c2);
|
||||||
|
const wSum = w1 + w2 + w3;
|
||||||
|
if (Math.abs(wSum) < 1e-6) {
|
||||||
|
vlog(`Y-Achse: Marker ${id} – Umkreis undefiniert`, 'warn');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const C = [
|
||||||
|
(w1*P1[0] + w2*P2[0] + w3*P3[0]) / wSum,
|
||||||
|
(w1*P1[1] + w2*P2[1] + w3*P3[1]) / wSum,
|
||||||
|
(w1*P1[2] + w2*P2[2] + w3*P3[2]) / wSum,
|
||||||
|
];
|
||||||
|
circumcenters.push({ id, C });
|
||||||
|
|
||||||
|
// Kreismittelpunkt (rose)
|
||||||
|
gYAxis.add(makeSphere(r2vArr(C), 0.007, 0xfb7185));
|
||||||
|
// Bogen-Linie B→C (cyan)
|
||||||
|
gYAxis.add(makeLine(r2vArr(P2), r2vArr(P3), 0x22d3ee, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (circumcenters.length === 0) {
|
||||||
|
vlog('Y-Achse: Keine gemeinsamen fremd-Marker in Pos A+B+C gefunden', 'warn');
|
||||||
|
window.parent.postMessage({ type: 'yaxis-measurement', axisDir: null }, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Achsenrichtung: Mittlere Normale (Vorzeichen angleichen)
|
||||||
|
const ref = normals[0];
|
||||||
|
const aligned = normals.map(n => {
|
||||||
|
const dot = n[0]*ref[0] + n[1]*ref[1] + n[2]*ref[2];
|
||||||
|
return dot >= 0 ? n : n.map(c => -c);
|
||||||
|
});
|
||||||
|
const meanN = [0, 1, 2].map(i => aligned.reduce((s, n) => s + n[i], 0) / aligned.length);
|
||||||
|
const meanNLen = Math.sqrt(meanN[0]**2 + meanN[1]**2 + meanN[2]**2);
|
||||||
|
const axisDir = meanN.map(c => c / meanNLen); // Roboter-Koordinaten
|
||||||
|
|
||||||
|
// Referenzpunkt: Schwerpunkt der Umkreismittelpunkte
|
||||||
|
const axisPoint = [0, 1, 2].map(i =>
|
||||||
|
circumcenters.reduce((s, c) => s + c.C[i], 0) / circumcenters.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// Achse als Linie ±500 mm visualisieren (magenta)
|
||||||
|
const L = 500;
|
||||||
|
const p1mm = axisPoint.map((v, i) => v - L * axisDir[i]);
|
||||||
|
const p2mm = axisPoint.map((v, i) => v + L * axisDir[i]);
|
||||||
|
gYAxis.add(makeLine(r2vArr(p1mm), r2vArr(p2mm), 0xe879f9, 0.9));
|
||||||
|
gYAxis.add(makeSphere(r2vArr(axisPoint), 0.011, 0xe879f9));
|
||||||
|
|
||||||
|
// Abweichung von der idealen Y-Achse [0,1,0] in Roboter-Koordinaten
|
||||||
|
const [ax, ay, az] = axisDir;
|
||||||
|
const tiltXY = Math.atan2(ax, ay) * 180 / Math.PI; // Kippung in XY-Ebene
|
||||||
|
const tiltYZ = Math.atan2(az, ay) * 180 / Math.PI; // Kippung in YZ-Ebene
|
||||||
|
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
|
||||||
|
const good = Math.abs(tiltXY) < 0.5 && Math.abs(tiltYZ) < 0.5;
|
||||||
|
|
||||||
|
vlog(`Y-Achse (${circumcenters.length} Marker): dir=[${axisDir.map(v => v.toFixed(4)).join(', ')}]`);
|
||||||
|
vlog(` Referenzpunkt: [${axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
|
||||||
|
vlog(` Abw. von Y-Achse: XY ${fmt(tiltXY)} YZ ${fmt(tiltYZ)}`, good ? 'ok' : 'warn');
|
||||||
|
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'yaxis-measurement',
|
||||||
|
axisDir,
|
||||||
|
axisPoint,
|
||||||
|
tiltXY,
|
||||||
|
tiltYZ,
|
||||||
|
numMarkers: circumcenters.length,
|
||||||
|
}, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Daten laden ───────────────────────────────────────────────────────────────
|
// ── Daten laden ───────────────────────────────────────────────────────────────
|
||||||
@@ -778,11 +913,44 @@ async function loadCompareData() {
|
|||||||
}
|
}
|
||||||
vlog(`Vergleich: ${markers.length} gesamt fremd=${_compareFremdMarkers.length} boardIDs=${boardIds.size}` +
|
vlog(`Vergleich: ${markers.length} gesamt fremd=${_compareFremdMarkers.length} boardIDs=${boardIds.size}` +
|
||||||
(_compareFremdMarkers.length ? ` (${_compareFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
|
(_compareFremdMarkers.length ? ` (${_compareFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
|
||||||
} catch (err) { vlog(`Vergleich Fehler: ${err}`, 'err'); }
|
} catch (err) { vlog(`Pos B Fehler: ${err}`, 'err'); }
|
||||||
buildCompareLines(); // Linien + Transparenz aktualisieren
|
buildCompareLines(); // Linien + Transparenz + Y-Achse aktualisieren
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run-Listen laden und beide Dropdowns befüllen. */
|
/**
|
||||||
|
* Pos C laden.
|
||||||
|
* Zeigt nur fremd-triangulierte Marker als cyan Kugeln.
|
||||||
|
* Danach wird Y-Achse neu berechnet (computeAndShowYAxis).
|
||||||
|
*/
|
||||||
|
async function loadPositionC() {
|
||||||
|
clearGroup(gPositionC);
|
||||||
|
_positionCFremdMarkers = [];
|
||||||
|
const selRun = document.getElementById('sel-run-c')?.value ?? '';
|
||||||
|
if (!selRun) {
|
||||||
|
vlog('Pos C: nicht gewählt');
|
||||||
|
computeAndShowYAxis(); // räumt gYAxis auf
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vlog(`Pos C: lade ${selRun} …`);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/board/latest?run=${encodeURIComponent(selRun)}`);
|
||||||
|
if (!r.ok) { vlog(`Pos C: HTTP ${r.status}`, 'err'); computeAndShowYAxis(); return; }
|
||||||
|
const data = await r.json();
|
||||||
|
const markers = data.measuredMarkers?.markers ?? [];
|
||||||
|
const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
|
||||||
|
for (const m of markers) {
|
||||||
|
if (!boardIds.has(m.marker_id)) {
|
||||||
|
_positionCFremdMarkers.push(m);
|
||||||
|
gPositionC.add(makeSphere(r2vArr(m.position_mm), 0.006, 0x22d3ee)); // cyan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vlog(`Pos C: ${markers.length} gesamt fremd=${_positionCFremdMarkers.length}` +
|
||||||
|
(_positionCFremdMarkers.length ? ` (${_positionCFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
|
||||||
|
} catch (err) { vlog(`Pos C Fehler: ${err}`, 'err'); }
|
||||||
|
computeAndShowYAxis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run-Listen laden und alle Dropdowns befüllen. */
|
||||||
async function initRunSelectors() {
|
async function initRunSelectors() {
|
||||||
try {
|
try {
|
||||||
const [r5, r10] = await Promise.all([
|
const [r5, r10] = await Promise.all([
|
||||||
@@ -800,28 +968,50 @@ async function initRunSelectors() {
|
|||||||
selP.innerHTML = '<option value="">⟳ aktuellster</option>' +
|
selP.innerHTML = '<option value="">⟳ aktuellster</option>' +
|
||||||
runs5.map(r => `<option value="${r}"${r === cur ? ' selected' : ''}>${r}</option>`).join('');
|
runs5.map(r => `<option value="${r}"${r === cur ? ' selected' : ''}>${r}</option>`).join('');
|
||||||
}
|
}
|
||||||
|
// ── Default-Verhalten per URL-Param steuern ──────────────────────────────
|
||||||
|
// ?defaults=a → nur Pos A bekommt Default (Board-Tab, Y-Achse-Tab)
|
||||||
|
// ?defaults=ab → Pos A + Pos B (X-Achsen-Tab)
|
||||||
|
// ?defaults=abc → alle drei (Arm-Tab)
|
||||||
|
// kein Param → wie "ab" (Rückwärtskompatibilität)
|
||||||
|
// ?compare=none → wie "a" (Rückwärtskompatibilität)
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const rawDef = params.get('defaults')
|
||||||
|
?? (params.get('compare') === 'none' ? 'a' : 'ab');
|
||||||
|
const defB = rawDef.includes('b');
|
||||||
|
const defC = rawDef.includes('c');
|
||||||
|
|
||||||
if (selC) {
|
if (selC) {
|
||||||
// URL-Param ?compare=none → kein Default-Vergleich (Board-Tab)
|
|
||||||
// Ohne den Param → zweiten neuesten Run vorwählen (X-Achsen-Tab)
|
|
||||||
const noCompare = new URLSearchParams(window.location.search).get('compare') === 'none';
|
|
||||||
const prevCompare = selC.value;
|
const prevCompare = selC.value;
|
||||||
selC.innerHTML = '<option value="">– keiner –</option>' +
|
selC.innerHTML = '<option value="">– keiner –</option>' +
|
||||||
runs10.map(r => `<option value="${r}">${r}</option>`).join('');
|
runs10.map(r => `<option value="${r}">${r}</option>`).join('');
|
||||||
if (prevCompare) {
|
if (prevCompare) {
|
||||||
selC.value = prevCompare; // bisher gewählten behalten
|
selC.value = prevCompare; // bisher gewählten behalten
|
||||||
} else if (!noCompare && runs10.length >= 2) {
|
} else if (defB && runs10.length >= 2) {
|
||||||
selC.value = runs10[1]; // zweiter neuester als Default (X-Achse)
|
selC.value = runs10[1]; // zweiter neuester als Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pos C ──
|
||||||
|
const selRunC = document.getElementById('sel-run-c');
|
||||||
|
if (selRunC) {
|
||||||
|
const prevC = selRunC.value;
|
||||||
|
selRunC.innerHTML = '<option value="">– keiner –</option>' +
|
||||||
|
runs10.map(r => `<option value="${r}">${r}</option>`).join('');
|
||||||
|
if (prevC) {
|
||||||
|
selRunC.value = prevC; // bisher gewählten behalten
|
||||||
|
} else if (defC && runs10.length >= 3) {
|
||||||
|
selRunC.value = runs10[2]; // dritter neuester als Default
|
||||||
}
|
}
|
||||||
// Andernfalls bleibt "– keiner –" aktiv (Board-Tab mit ?compare=none)
|
|
||||||
}
|
}
|
||||||
} catch { /* offline oder noch keine Runs */ }
|
} catch { /* offline oder noch keine Runs */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vollständige Initialisierung: Selektoren → Basis → Vergleich (sequenziell!) */
|
/** Vollständige Initialisierung: Selektoren → Pos A → Pos B → Pos C (sequenziell!) */
|
||||||
async function initAll() {
|
async function initAll() {
|
||||||
await initRunSelectors();
|
await initRunSelectors();
|
||||||
await loadData(); // setzt _primaryFremdMarkers
|
await loadData(); // setzt _primaryFremdMarkers
|
||||||
await loadCompareData(); // setzt _compareFremdMarkers + baut Linien
|
await loadCompareData(); // setzt _compareFremdMarkers + baut Linien + Y-Achse (no-op)
|
||||||
|
await loadPositionC(); // setzt _positionCFremdMarkers + berechnet Y-Achse
|
||||||
}
|
}
|
||||||
|
|
||||||
initAll();
|
initAll();
|
||||||
@@ -829,8 +1019,12 @@ document.getElementById('btnReload').addEventListener('click', initAll);
|
|||||||
document.getElementById('sel-run-primary')?.addEventListener('change', async () => {
|
document.getElementById('sel-run-primary')?.addEventListener('change', async () => {
|
||||||
await loadData();
|
await loadData();
|
||||||
await loadCompareData();
|
await loadCompareData();
|
||||||
|
await loadPositionC();
|
||||||
});
|
});
|
||||||
document.getElementById('sel-run-compare')?.addEventListener('change', loadCompareData);
|
document.getElementById('sel-run-compare')?.addEventListener('change', async () => {
|
||||||
|
await loadCompareData(); // baut Linien + ruft computeAndShowYAxis auf
|
||||||
|
});
|
||||||
|
document.getElementById('sel-run-c')?.addEventListener('change', loadPositionC);
|
||||||
window.addEventListener('message', async (e) => {
|
window.addEventListener('message', async (e) => {
|
||||||
if (e.data?.type === 'reload') await initAll();
|
if (e.data?.type === 'reload') await initAll();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,8 +22,11 @@
|
|||||||
<nav class="tab-sidebar" id="tabSidebar">
|
<nav class="tab-sidebar" id="tabSidebar">
|
||||||
<button class="tab-btn active" data-tab="camera-npz" data-src="/calibration_camera.html">Camera NPZ</button>
|
<button class="tab-btn active" data-tab="camera-npz" data-src="/calibration_camera.html">Camera NPZ</button>
|
||||||
<button class="tab-btn" data-tab="board" data-src="/calibration_board.html">Board</button>
|
<button class="tab-btn" data-tab="board" data-src="/calibration_board.html">Board</button>
|
||||||
<button class="tab-btn" data-tab="robot-x-axis" data-src="/calibration_xaxis.html">Robot X Axis</button>
|
<button class="tab-btn" data-tab="robot-x-axis" data-src="/calibration_xaxis.html">Robot X Axis</button>
|
||||||
<button class="tab-btn" data-tab="arm" data-src="/calibration_arm.html">Arm1 / Arm2</button>
|
<button class="tab-btn" data-tab="arm1" data-src="/calibration_arm.html">Arm1 – Y</button>
|
||||||
|
<button class="tab-btn" data-tab="arm2" data-src="/calibration_arm2.html">Arm2 – Z</button>
|
||||||
|
<button class="tab-btn" data-tab="elbow" data-src="/calibration_elbow.html">Elbow</button>
|
||||||
|
<button class="tab-btn" data-tab="hand" data-src="/calibration_hand.html">Hand</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- CONTENT (Panels werden lazy per fetch befüllt) -->
|
<!-- CONTENT (Panels werden lazy per fetch befüllt) -->
|
||||||
@@ -31,7 +34,10 @@
|
|||||||
<div class="tab-panel active" id="tab-camera-npz"></div>
|
<div class="tab-panel active" id="tab-camera-npz"></div>
|
||||||
<div class="tab-panel" id="tab-board"></div>
|
<div class="tab-panel" id="tab-board"></div>
|
||||||
<div class="tab-panel" id="tab-robot-x-axis"></div>
|
<div class="tab-panel" id="tab-robot-x-axis"></div>
|
||||||
<div class="tab-panel" id="tab-arm"></div>
|
<div class="tab-panel" id="tab-arm1"></div>
|
||||||
|
<div class="tab-panel" id="tab-arm2"></div>
|
||||||
|
<div class="tab-panel" id="tab-elbow"></div>
|
||||||
|
<div class="tab-panel" id="tab-hand"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /.calib-body -->
|
</div><!-- /.calib-body -->
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ async function loadPanel(tab, src) {
|
|||||||
if (tab === 'camera-npz') initCameraNpz();
|
if (tab === 'camera-npz') initCameraNpz();
|
||||||
else if (tab === 'board') initBoard();
|
else if (tab === 'board') initBoard();
|
||||||
else if (tab === 'robot-x-axis') initXAxis();
|
else if (tab === 'robot-x-axis') initXAxis();
|
||||||
|
else if (tab === 'arm1') initArm('arm1');
|
||||||
|
else if (tab === 'arm2') initArm('arm2');
|
||||||
|
else if (tab === 'elbow') initArm('elbow');
|
||||||
|
else if (tab === 'hand') initArm('hand');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('tab-' + tab).innerHTML =
|
document.getElementById('tab-' + tab).innerHTML =
|
||||||
@@ -496,6 +500,75 @@ function initXAxis() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tabs: Arm1 / Arm2 / Elbow / Hand (generisch) ────────────────────────────
|
||||||
|
// Alle Gelenk-Tabs teilen dieselbe Init-Logik – der Tab-Name (arm1, arm2, …)
|
||||||
|
// wird als Prefix für Element-IDs und Viewer-Frame-ID genutzt.
|
||||||
|
|
||||||
|
function initArm(tab) {
|
||||||
|
const logEl = document.getElementById(`log-${tab}`);
|
||||||
|
const frameEl = document.getElementById(`${tab}-viewer-frame`);
|
||||||
|
const runBtn = document.getElementById(`btn-${tab}-run`);
|
||||||
|
const lastRunEl = document.getElementById(`${tab}-last-run`);
|
||||||
|
|
||||||
|
if (!logEl) return; // Panel noch nicht geladen
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
const ts = new Date().toLocaleTimeString('de-CH');
|
||||||
|
logEl.value += `[${ts}] ${msg}\n`;
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Board erkennen"-Button
|
||||||
|
if (runBtn) {
|
||||||
|
runBtn.addEventListener('click', async () => {
|
||||||
|
log('Board-Erkennung …');
|
||||||
|
runBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/board/run', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const raw = await response.text().catch(() => '');
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(raw).error || raw; }
|
||||||
|
catch { msg = raw.slice(0, 300) || `HTTP ${response.status}`; }
|
||||||
|
log(`❌ HTTP ${response.status}: ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await readSseStream(response, log, (evt) => {
|
||||||
|
if (evt.exitCode === 0) {
|
||||||
|
log('✅ Board-Run abgeschlossen.');
|
||||||
|
if (evt.runDir) {
|
||||||
|
if (lastRunEl) lastRunEl.textContent = evt.runDir;
|
||||||
|
if (frameEl?.contentWindow) frameEl.contentWindow.postMessage({ type: 'reload' }, '*');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`❌ Beendet mit Exit-Code ${evt.exitCode}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log(`❌ Fehler: ${err}`);
|
||||||
|
} finally {
|
||||||
|
runBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Achsen-Messung vom Viewer empfangen
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
if (!frameEl || e.source !== frameEl.contentWindow) return;
|
||||||
|
const msg = e.data;
|
||||||
|
if (msg?.type === 'yaxis-measurement' && Array.isArray(msg.axisDir)) {
|
||||||
|
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
|
||||||
|
log(`📐 Achse (${msg.numMarkers} Marker): dir=[${msg.axisDir.map(v => v.toFixed(4)).join(', ')}]` +
|
||||||
|
` XY=${fmt(msg.tiltXY)} YZ=${fmt(msg.tiltYZ)}`);
|
||||||
|
log(` Referenzpunkt: [${msg.axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tab: Board (shared helpers) ───────────────────────────────────────────────
|
// ── Tab: Board (shared helpers) ───────────────────────────────────────────────
|
||||||
|
|
||||||
/** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */
|
/** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
<div class="sections">
|
<div class="sections">
|
||||||
|
|
||||||
<div class="section full">
|
<div class="section full">
|
||||||
<h2>Arm1 / Arm2 <span class="status-badge open">offen</span></h2>
|
<h2>Arm1 – Y-Achse <span class="status-badge open">offen</span></h2>
|
||||||
<div class="placeholder-note">
|
<div class="info-grid" style="margin-top:14px">
|
||||||
Ziel: Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen.
|
<span class="info-label">Ziel</span>
|
||||||
Arm fährt in mechanische Nullposition, Kamera prüft die tatsächliche Pose,
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
Offset wird berechnet und gespeichert.<br><br>
|
Y-Rotationsachse von Arm1 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
|
||||||
Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto →
|
</span>
|
||||||
Winkel · Offset-Korrektur speichern.<br><br>
|
<span class="info-label">Ablauf</span>
|
||||||
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Board erkennen (Pos A) → Arm1 drehen → Board erkennen (Pos B) → Arm1 drehen → Board erkennen (Pos C)
|
||||||
|
→ Viewer zeigt berechnete Rotationsachse (magenta)
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Letzter Run</span>
|
||||||
|
<span class="info-value" id="arm1-last-run">–</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls" style="margin-top: 14px;">
|
<div class="controls" style="margin-top:16px">
|
||||||
<button disabled>Arm1 → Nullpos</button>
|
<button id="btn-arm1-run">Board erkennen</button>
|
||||||
<button disabled>Foto Arm1</button>
|
|
||||||
<button disabled>Arm2 → Nullpos</button>
|
|
||||||
<button disabled>Foto Arm2</button>
|
|
||||||
<button disabled>Offsets berechnen</button>
|
|
||||||
<button disabled>Speichern</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section full">
|
<div class="section full">
|
||||||
<h2>Ausgabe / Log</h2>
|
<h2>Ausgabe / Log</h2>
|
||||||
<textarea id="log-arm" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
<textarea id="log-arm1" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Board-Viewer</h2>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
|
||||||
|
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan) –
|
||||||
|
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
|
||||||
|
</p>
|
||||||
|
<iframe
|
||||||
|
id="arm1-viewer-frame"
|
||||||
|
src="/boardViewer.html?defaults=abc"
|
||||||
|
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
||||||
|
title="Board-Viewer (Arm1)"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
42
public/calibration_arm2.html
Normal file
42
public/calibration_arm2.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="sections">
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Arm2 – Z-Achse <span class="status-badge open">offen</span></h2>
|
||||||
|
<div class="info-grid" style="margin-top:14px">
|
||||||
|
<span class="info-label">Ziel</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Z-Rotationsachse von Arm2 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Ablauf</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Board erkennen (Pos A) → Arm2 drehen → Board erkennen (Pos B) → Arm2 drehen → Board erkennen (Pos C)
|
||||||
|
→ Viewer zeigt berechnete Rotationsachse (magenta)
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Letzter Run</span>
|
||||||
|
<span class="info-value" id="arm2-last-run">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls" style="margin-top:16px">
|
||||||
|
<button id="btn-arm2-run">Board erkennen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Ausgabe / Log</h2>
|
||||||
|
<textarea id="log-arm2" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Board-Viewer</h2>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
|
||||||
|
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan) –
|
||||||
|
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
|
||||||
|
</p>
|
||||||
|
<iframe
|
||||||
|
id="arm2-viewer-frame"
|
||||||
|
src="/boardViewer.html?defaults=abc"
|
||||||
|
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
||||||
|
title="Board-Viewer (Arm2)"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<iframe
|
<iframe
|
||||||
id="board-viewer-frame"
|
id="board-viewer-frame"
|
||||||
src="/boardViewer.html?compare=none"
|
src="/boardViewer.html?defaults=a"
|
||||||
style="width: 100%; height: 740px; border: 1px solid #334155; border-radius: 6px; background: #0d0f13; display: block;"
|
style="width: 100%; height: 740px; border: 1px solid #334155; border-radius: 6px; background: #0d0f13; display: block;"
|
||||||
title="Board-Viewer"
|
title="Board-Viewer"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
42
public/calibration_elbow.html
Normal file
42
public/calibration_elbow.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="sections">
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Elbow – Rotation <span class="status-badge open">offen</span></h2>
|
||||||
|
<div class="info-grid" style="margin-top:14px">
|
||||||
|
<span class="info-label">Ziel</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Rotationsachse des Elbow-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Ablauf</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Board erkennen (Pos A) → Elbow drehen → Board erkennen (Pos B) → Elbow drehen → Board erkennen (Pos C)
|
||||||
|
→ Viewer zeigt berechnete Rotationsachse (magenta)
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Letzter Run</span>
|
||||||
|
<span class="info-value" id="elbow-last-run">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls" style="margin-top:16px">
|
||||||
|
<button id="btn-elbow-run">Board erkennen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Ausgabe / Log</h2>
|
||||||
|
<textarea id="log-elbow" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Board-Viewer</h2>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
|
||||||
|
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan) –
|
||||||
|
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
|
||||||
|
</p>
|
||||||
|
<iframe
|
||||||
|
id="elbow-viewer-frame"
|
||||||
|
src="/boardViewer.html?defaults=abc"
|
||||||
|
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
||||||
|
title="Board-Viewer (Elbow)"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
42
public/calibration_hand.html
Normal file
42
public/calibration_hand.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="sections">
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Hand – Rotation <span class="status-badge open">offen</span></h2>
|
||||||
|
<div class="info-grid" style="margin-top:14px">
|
||||||
|
<span class="info-label">Ziel</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Rotationsachse des Hand-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Ablauf</span>
|
||||||
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
|
Board erkennen (Pos A) → Hand drehen → Board erkennen (Pos B) → Hand drehen → Board erkennen (Pos C)
|
||||||
|
→ Viewer zeigt berechnete Rotationsachse (magenta)
|
||||||
|
</span>
|
||||||
|
<span class="info-label">Letzter Run</span>
|
||||||
|
<span class="info-value" id="hand-last-run">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls" style="margin-top:16px">
|
||||||
|
<button id="btn-hand-run">Board erkennen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Ausgabe / Log</h2>
|
||||||
|
<textarea id="log-hand" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section full">
|
||||||
|
<h2>Board-Viewer</h2>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
|
||||||
|
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan) –
|
||||||
|
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
|
||||||
|
</p>
|
||||||
|
<iframe
|
||||||
|
id="hand-viewer-frame"
|
||||||
|
src="/boardViewer.html?defaults=abc"
|
||||||
|
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
||||||
|
title="Board-Viewer (Hand)"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<iframe
|
<iframe
|
||||||
id="xaxis-viewer-frame"
|
id="xaxis-viewer-frame"
|
||||||
src="/boardViewer.html"
|
src="/boardViewer.html?defaults=ab"
|
||||||
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
|
||||||
title="Board-Viewer (X-Achse)"
|
title="Board-Viewer (X-Achse)"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
Reference in New Issue
Block a user