// ==UserScript==
// @name 标注校验插件
// @namespace http://tampermonkey.net/
// @version 12.8
// @description 2D遮挡+3D可见性+3D遮挡校验,错误项点击定位+自动创建批注
// @match http://121.37.95.217:8080/pointcloud/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const VIEW_NAMES = {
'front_fisheye': '前鱼眼', 'front_wide': '前广角',
'back_fisheye': '后鱼眼', 'left_fisheye': '左鱼眼', 'right_fisheye': '右鱼眼'
};
let isAutoValidating = false;
let lastFrameKey = '';
let autoMode = false;
let panel = null;
let panelReady = false;
// ========== 1. Canvas文字拦截(保留hook但已知无效) ==========
let allCanvasTexts = [];
let hookInstalled = false;
function installCanvasHook() {
if (hookInstalled) return;
hookInstalled = true;
const proto = CanvasRenderingContext2D.prototype;
const origFillText = proto.fillText;
const origStrokeText = proto.strokeText;
proto.fillText = function (text, x, y, maxWidth) {
try {
if (text && typeof text === 'string' && text.length > 0) {
let canvas = this.canvas;
let camId = '';
if (canvas) {
camId = canvas.getAttribute('data-value') || canvas.dataset.value || '';
if (!camId) {
let parent = canvas.parentElement;
let depth = 0;
while (parent && !camId && depth < 5) {
camId = parent.getAttribute('data-value') || parent.dataset.value || '';
parent = parent.parentElement;
depth++;
}
}
}
allCanvasTexts.push({ text: text, x: Math.round(x), y: Math.round(y), camId: camId || 'unknown' });
}
} catch (e) { }
return origFillText.apply(this, arguments);
};
proto.strokeText = function (text, x, y, maxWidth) {
try {
if (text && typeof text === 'string' && text.length > 0) {
let canvas = this.canvas;
let camId = '';
if (canvas) {
camId = canvas.getAttribute('data-value') || canvas.dataset.value || '';
if (!camId) {
let parent = canvas.parentElement;
let depth = 0;
while (parent && !camId && depth < 5) {
camId = parent.getAttribute('data-value') || parent.dataset.value || '';
parent = parent.parentElement;
depth++;
}
}
}
allCanvasTexts.push({ text: text, x: Math.round(x), y: Math.round(y), camId: camId || 'unknown' });
}
} catch (e) { }
return origStrokeText.apply(this, arguments);
};
}
installCanvasHook();
function clearCanvasText() {
allCanvasTexts = [];
}
// ========== 2. Vue Store获取 ==========
function getVueStore() {
let el = document.getElementById('app') || document.querySelector('#app');
if (!el) return null;
if (el.__vue_app__) {
try {
let gp = el.__vue_app__.config.globalProperties;
if (gp && gp.$store) return gp.$store;
} catch (e) { }
}
if (el.__vue__) {
try {
if (el.__vue__.$store) return el.__vue__.$store;
} catch (e) { }
}
return null;
}
const VALID_OCCLUSION_CODES = new Set(['10', '11', '12', '13']);
// ========== 2b. 2D遮挡校验(通过平台getShowAttrs获取显示文字) ==========
function check2DOcclusion() {
let store = getVueStore();
if (!store || !store.state || !store.state.marks) {
return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] };
}
let marks = store.state.marks;
if (!marks || typeof marks !== 'object') {
return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] };
}
// 获取平台自己的getShowAttrs getter
let getShowAttrs = null;
try { getShowAttrs = store.getters['config/getShowAttrs']; } catch (e) { }
// 获取className getter(获取中文类名)
let getClassName = null;
try { getClassName = store.getters['config/className']; } catch (e) { }
// 获取classId getter
let getClassId = null;
try { getClassId = store.getters['marks/classId']; } catch (e) { }
// 获取attrsDirection
let attrsDirection = false;
try { attrsDirection = store.state.status.attrsDirection; } catch (e) { }
// 找当前帧ID
let curFrameId = null;
try {
curFrameId = store.state.status.curFrameId;
} catch (e) { }
if (!curFrameId) {
// fallback: 找出现次数最多的帧ID
let frameIds = {};
for (let catKey in marks) {
let category = marks[catKey];
if (!category || typeof category !== 'object') continue;
for (let instKey in category) {
let instance = category[instKey];
if (!instance || typeof instance !== 'object') continue;
if (instance['3d'] && typeof instance['3d'] === 'object') {
for (let fid in instance['3d']) {
frameIds[fid] = (frameIds[fid] || 0) + 1;
}
}
}
}
let maxCount = 0;
for (let fid in frameIds) {
if (frameIds[fid] > maxCount) { maxCount = frameIds[fid]; curFrameId = fid; }
}
}
if (!curFrameId) {
return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] };
}
let errors = [];
let checkedViews = new Set();
for (let catKey in marks) {
let category = marks[catKey];
if (!category || typeof category !== 'object') continue;
for (let instKey in category) {
let instance = category[instKey];
if (!instance || typeof instance !== 'object') continue;
let d2 = instance['2d'];
if (!d2 || typeof d2 !== 'object') continue;
let frame2d = d2[curFrameId];
if (!frame2d) continue;
let boxList = [];
if (Array.isArray(frame2d)) {
boxList = frame2d;
} else if (typeof frame2d === 'object' && frame2d !== null) {
for (let k in frame2d) {
let v = frame2d[k];
if (Array.isArray(v)) boxList = boxList.concat(v);
else if (typeof v === 'object' && v !== null) boxList.push(v);
}
}
for (let i = 0; i < boxList.length; i++) {
let box = boxList[i];
if (!box || typeof box !== 'object') continue;
let camera = box.camera || '';
let viewName = VIEW_NAMES[camera] || camera || '未知视角';
checkedViews.add(viewName);
let class_name = instance.class || instKey;
let trackId = instKey.split('__')[0].replace(/^[^-]+-/, '');
let display_name = class_name + '-' + trackId;
if (getClassName) {
try {
let classId = null;
if (getClassId) classId = getClassId(instKey);
if (!classId && instance.classId) classId = instance.classId;
let toolType = box.type || instance.type || 'rect2d';
if (classId) {
let cn = getClassName(classId, toolType);
if (cn) display_name = cn + '-' + trackId;
}
} catch (e) { }
}
let attrs = box.attrs;
if (!attrs) continue;
if (typeof attrs === 'string') { try { attrs = JSON.parse(attrs); } catch (e) { } }
// 用平台的getShowAttrs获取显示文字
let displayText = null;
if (getShowAttrs) {
try {
let classId = null;
if (getClassId) {
classId = getClassId(instKey);
}
let boxType = box.originalType || box.type || instance.type || 'rect2d';
let result = getShowAttrs(boxType, classId, attrs, !attrsDirection);
if (Array.isArray(result)) {
displayText = result.join('\n');
} else if (typeof result === 'string') {
displayText = result;
}
} catch (e) {
console.log('[校验插件] getShowAttrs调用失败:', e.message, 'instKey:', instKey);
}
}
if (displayText) {
// 检查显示文字中"遮挡:"后面是数字还是汉字
let occMatch = displayText.match(/遮挡[::]\s*(.{1,30})/);
if (occMatch) {
let occValue = occMatch[1].trim();
occValue = occValue.split(/截断|运动|车门|可见性/)[0].trim();
if (!occValue.includes('可') && /^\d/.test(occValue)) {
errors.push({
type: '2d', view: viewName, instKey: instKey,
displayName: display_name,
message: `[2D] ${viewName} - ${display_name}:遮挡显示为"${occValue}"`
});
}
}
} else {
// fallback: 直接检查attrs里的遮挡编码
let occlusion = null;
if (attrs.occlusion) {
occlusion = Array.isArray(attrs.occlusion) ? attrs.occlusion[0] : attrs.occlusion;
}
if (occlusion !== null && occlusion !== undefined && occlusion !== '') {
let occlusionStr = String(occlusion);
if (!VALID_OCCLUSION_CODES.has(occlusionStr)) {
errors.push({
type: '2d', view: viewName, instKey: instKey,
displayName: display_name,
message: `[2D] ${viewName} - ${display_name}:遮挡值"${occlusionStr}"`
});
}
}
}
}
}
}
return { errors, views: Array.from(checkedViews) };
}
// ========== 3. 3D可见性 + 遮挡校验 ==========
function check3DVisibility() {
let labels3D = document.querySelectorAll('.threejs-label');
let missing = [];
let occlusionErrors = [];
let count = 0;
labels3D.forEach((label, idx) => {
let text = (label.textContent || '').trim();
if (text.length === 0) return;
if (!text.includes(':') && !text.includes(':')) return;
count++;
let firstLine = text.split('\n')[0].trim();
// 检查可见性字段
if (!text.includes('可见性')) {
missing.push({ labelText: firstLine, labelIndex: idx, message: `${firstLine}:缺少"可见性"字段` });
}
// 检查遮挡值:遮挡值必须包含"可见"两字(如"可见100%"),否则为错误
let occMatch = text.match(/遮挡[::]\s*(.{1,30})/);
if (occMatch) {
let occValue = occMatch[1].trim();
occValue = occValue.split(/[\n_]|截断|运动|车门|种类|可见性/)[0].trim();
if (!occValue.includes('可见')) {
occlusionErrors.push({ labelText: firstLine, labelIndex: idx, message: `${firstLine}:遮挡显示为"${occValue}"` });
}
}
});
return { count, missing, occlusionErrors };
}
// ========== 4. 注入CSS ==========
function injectStyles() {
if (document.getElementById('chk-styles')) return;
const style = document.createElement('style');
style.id = 'chk-styles';
style.textContent = `
#chk-panel {
position: fixed; top: 80px; left: 320px;
width: 340px; max-height: 70vh;
background: linear-gradient(145deg, #1a1a2e 0%, #16213e 100%);
color: #e0e0e0;
border: 1px solid rgba(0,210,255,0.2);
border-radius: 12px; z-index: 999999;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
font-size: 13px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden; transition: box-shadow 0.2s;
}
#chk-panel:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 20px rgba(0,210,255,0.15); }
#chk-header {
background: linear-gradient(135deg, #0f3460 0%, #1a1a40 100%);
padding: 10px 14px; display: flex; justify-content: space-between;
align-items: center; cursor: move; user-select: none;
border-bottom: 1px solid rgba(0,210,255,0.15);
}
#chk-header:active { cursor: grabbing; }
.chk-title {
font-weight: bold; font-size: 14px; color: #00d2ff;
text-shadow: 0 0 10px rgba(0,210,255,0.3);
display: flex; align-items: center; gap: 6px;
}
.chk-title-dot {
width: 8px; height: 8px; background: #00d2ff;
border-radius: 50%; box-shadow: 0 0 8px #00d2ff;
animation: chk-pulse 2s ease-in-out infinite;
}
@keyframes chk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.chk-btn-check {
background: linear-gradient(135deg, #00d2ff 0%, #0288d1 100%);
color: #fff; border: none; padding: 5px 14px;
border-radius: 6px; cursor: pointer; font-size: 12px;
font-weight: bold; transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,210,255,0.3);
}
.chk-btn-check:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,210,255,0.5); }
.chk-btn-check:active { transform: translateY(0); }
.chk-btn-check:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.chk-btn-toggle {
background: rgba(255,255,255,0.08); color: #aaa;
border: 1px solid rgba(255,255,255,0.1);
width: 26px; height: 26px; border-radius: 6px;
cursor: pointer; font-size: 14px;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; margin-left: 6px;
}
.chk-btn-toggle:hover { background: rgba(255,255,255,0.15); color: #fff; }
.chk-btn-mode {
border: 1px solid rgba(255,193,7,0.3);
background: rgba(255,193,7,0.15); color: #ffc107;
padding: 4px 10px; border-radius: 6px; cursor: pointer;
font-size: 11px; font-weight: bold; transition: all 0.2s;
margin-left: 6px; min-width: 36px;
}
.chk-btn-mode:hover { transform: translateY(-1px); }
.chk-btn-mode.auto {
background: linear-gradient(135deg, #00d2ff 0%, #0288d1 100%);
color: #fff; border: none;
box-shadow: 0 2px 8px rgba(0,210,255,0.3);
}
.chk-status-badge {
display: none; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 8px; font-size: 11px; font-weight: bold;
margin-left: 8px; white-space: nowrap;
}
.chk-status-badge.chk-status-validating {
display: flex; background: rgba(255,193,7,0.15); color: #ffc107;
border: 1px solid rgba(255,193,7,0.3);
}
.chk-status-badge.chk-status-done {
display: flex; background: rgba(78,204,163,0.15); color: #4ecca3;
border: 1px solid rgba(78,204,163,0.3);
}
.chk-status-badge.chk-status-error {
display: flex; background: rgba(233,69,96,0.15); color: #e94560;
border: 1px solid rgba(233,69,96,0.3);
}
#chk-content { padding: 12px 14px; max-height: calc(70vh - 50px); overflow-y: auto; }
#chk-content::-webkit-scrollbar { width: 6px; }
#chk-content::-webkit-scrollbar-track { background: transparent; }
#chk-content::-webkit-scrollbar-thumb { background: rgba(0,210,255,0.3); border-radius: 3px; }
#chk-content::-webkit-scrollbar-thumb:hover { background: rgba(0,210,255,0.5); }
.chk-stats {
margin-bottom: 10px; padding: 10px 12px;
background: rgba(15,52,96,0.4); border-radius: 8px;
border: 1px solid rgba(255,255,255,0.05);
}
.chk-stats-title {
color: #00d2ff; font-weight: bold; margin-bottom: 6px;
font-size: 12px; text-transform: uppercase; letter-spacing: 1px;
}
.chk-stats-body { color: #8892b0; font-size: 12px; line-height: 1.8; }
.chk-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: bold; }
.chk-badge-ok { background: rgba(78,204,163,0.15); color: #4ecca3; border: 1px solid rgba(78,204,163,0.3); }
.chk-badge-err { background: rgba(233,69,96,0.15); color: #e94560; border: 1px solid rgba(233,69,96,0.3); }
.chk-pass { color: #4ecca3; text-align: center; padding: 24px 0; font-size: 15px; font-weight: bold; }
.chk-warn-header {
color: #e94560; font-weight: bold; margin: 10px 0 6px;
font-size: 13px; display: flex; align-items: center; gap: 4px;
}
.chk-warn-item {
background: rgba(233,69,96,0.08); border-left: 3px solid #e94560;
padding: 8px 10px; margin-bottom: 5px; border-radius: 0 6px 6px 0;
font-size: 13px; line-height: 1.6; color: #ccc;
transition: background 0.2s, transform 0.1s;
cursor: pointer; position: relative;
}
.chk-warn-item:hover { background: rgba(233,69,96,0.18); transform: translateX(2px); }
.chk-warn-item:active { transform: translateX(0); }
.chk-warn-item::after {
content: '点击定位'; position: absolute; right: 8px; top: 50%;
transform: translateY(-50%); font-size: 10px; color: #8892b0;
opacity: 0; transition: opacity 0.2s;
}
.chk-warn-item:hover::after { opacity: 1; }
.chk-warn-item.chk-located {
background: rgba(255,193,7,0.1);
border-left-color: #ffc107;
}
.chk-warn-item.chk-located::before {
content: '📍';
margin-right: 4px;
font-size: 12px;
}
.chk-loading { color: #8892b0; text-align: center; padding: 24px 0; }
@keyframes chk-shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.chk-shake { animation: chk-shake 0.5s ease-in-out; }
`;
document.head.appendChild(style);
}
// ========== 5. 创建面板 ==========
function createPanel() {
if (panel && document.body.contains(panel)) return panel;
injectStyles();
panel = document.createElement('div');
panel.id = 'chk-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
document.getElementById('chk-btn-check').addEventListener('click', function () { validate(false); });
document.getElementById('chk-btn-toggle').addEventListener('click', togglePanel);
document.getElementById('chk-btn-mode').addEventListener('click', function () {
autoMode = !autoMode;
this.textContent = autoMode ? '自动' : '手动';
this.className = 'chk-btn-mode' + (autoMode ? ' auto' : '');
});
makeDraggable();
panelReady = true;
return panel;
}
// ========== 6. 拖拽 ==========
function makeDraggable() {
const header = document.getElementById('chk-header');
let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;
header.addEventListener('mousedown', function (e) {
if (e.target.tagName === 'BUTTON') return;
isDragging = true; startX = e.clientX; startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left; startTop = rect.top; e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!isDragging) return;
let newLeft = startLeft + (e.clientX - startX);
let newTop = startTop + (e.clientY - startY);
newLeft = Math.max(-260, Math.min(newLeft, window.innerWidth - 80));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - 50));
panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = 'auto';
});
document.addEventListener('mouseup', function () { isDragging = false; });
}
function togglePanel() {
const content = document.getElementById('chk-content');
const btn = document.getElementById('chk-btn-toggle');
if (content.style.display === 'none') { content.style.display = ''; btn.innerHTML = '−'; }
else { content.style.display = 'none'; btn.innerHTML = '+'; }
}
// ========== 7. 渲染结果 ==========
let currentErrors = [];
function renderResults(errors, info, isAuto) {
if (!panelReady) createPanel();
const content = document.getElementById('chk-content');
const btn = document.getElementById('chk-btn-check');
btn.textContent = '校验当前帧'; btn.disabled = false;
currentErrors = errors;
let hasErrors = errors.length > 0;
let html = '';
let statsBorder = hasErrors ? 'rgba(233,69,96,0.3)' : 'rgba(78,204,163,0.3)';
let statsColor = hasErrors ? '#e94560' : '#4ecca3';
let statusBadge = hasErrors
? '有异常'
: '通过';
if (isAuto) statusBadge += ' 自动';
html += `
校验结果
状态: ${statusBadge}
${info.join('
')}
`;
if (hasErrors) {
html += ``;
for (let i = 0; i < errors.length; i++) {
let err = errors[i];
let errKey = err.type === '3d'
? '3d_' + err.labelText
: '2d_' + err.view + '_' + err.instKey;
let locatedClass = errKey === lastLocatedKey ? ' chk-located' : '';
html += `${err.message}
`;
}
} else {
html += `✓ 校验通过,无问题
`;
}
content.innerHTML = html;
btn.textContent = '✓ 校验完成';
btn.disabled = false;
let statusBadgeEl = document.getElementById('chk-status-badge');
if (statusBadgeEl) {
if (hasErrors) { statusBadgeEl.className = 'chk-status-badge chk-status-error'; statusBadgeEl.textContent = '校验已完成'; }
else { statusBadgeEl.className = 'chk-status-badge chk-status-done'; statusBadgeEl.textContent = '校验已完成'; }
}
if (hasErrors && !isAuto) {
panel.classList.remove('chk-shake');
void panel.offsetWidth;
panel.classList.add('chk-shake');
}
content.querySelectorAll('.chk-warn-item').forEach(item => {
item.addEventListener('click', function () {
let idx = parseInt(this.getAttribute('data-err-idx'));
let errKey = this.getAttribute('data-err-key');
lastLocatedKey = errKey;
content.querySelectorAll('.chk-warn-item').forEach(el => el.classList.remove('chk-located'));
this.classList.add('chk-located');
locateError(currentErrors[idx]);
});
});
}
let lastLocatedKey = null;
function locateError(err) {
if (!err) return;
let store = getVueStore();
if (err.type === '3d') {
let trackId = null;
let classId = null;
let match = err.labelText.match(/^(.+?)-(\d+)$/);
if (match) {
trackId = match[2];
let className = match[1];
if (store && store.getters) {
try {
for (let cat in store.state.marks) {
let catData = store.state.marks[cat];
if (catData && catData[trackId]) {
if (catData[trackId].class && catData[trackId].class === className) {
classId = catData[trackId].classId || null;
}
break;
}
}
} catch (e) { }
}
}
if (trackId && store && store.commit) {
try {
store.commit('status/selectMarkInfo', [trackId, classId, '3d', null, true]);
} catch (e) {
console.log('[校验插件] 3D定位(Vuex)失败:', e.message);
let labels = document.querySelectorAll('.threejs-label');
for (let i = 0; i < labels.length; i++) {
let firstLine = (labels[i].textContent || '').split('\n')[0].trim();
if (firstLine === err.labelText) {
labels[i].click();
break;
}
}
}
}
lastLocatedKey = '3d_' + err.labelText;
setTimeout(() => {
let labels = document.querySelectorAll('.threejs-label');
for (let i = 0; i < labels.length; i++) {
let firstLine = (labels[i].textContent || '').split('\n')[0].trim();
if (firstLine === err.labelText) {
let label = labels[i];
label.style.transition = 'box-shadow 0.3s';
label.style.boxShadow = '0 0 0 4px #e94560, 0 0 20px 8px rgba(233,69,96,0.6)';
label.style.borderRadius = '6px';
let count = 0;
let blink = setInterval(() => {
count++;
if (count % 2 === 0) {
label.style.boxShadow = '0 0 0 4px #e94560, 0 0 20px 8px rgba(233,69,96,0.6)';
} else {
label.style.boxShadow = '0 0 0 2px #4ecca3, 0 0 10px 4px rgba(78,204,163,0.4)';
}
if (count >= 6) {
clearInterval(blink);
setTimeout(() => {
label.style.boxShadow = '';
label.style.borderRadius = '';
}, 1000);
}
}, 300);
break;
}
}
}, 500);
} else if (err.type === '2d') {
let store = getVueStore();
if (store && store.commit) {
try {
let cameraKey = err.cameraKey || null;
if (!cameraKey) {
for (let k in VIEW_NAMES) {
if (VIEW_NAMES[k] === err.view) {
cameraKey = k;
break;
}
}
}
let classId = err.classId || null;
if (!classId && store.getters && store.getters['marks/classId']) {
try { classId = store.getters['marks/classId'](err.instKey); } catch (e) { }
}
store.commit('status/selectMarkInfo', [err.instKey, classId, '2d', cameraKey, true]);
lastLocatedKey = '2d_' + err.view + '_' + err.instKey;
} catch (e) {
console.log('[校验插件] 2D定位失败:', e.message);
}
}
}
if (autoMode) {
setTimeout(() => { autoCreateAnnotation(); }, 800);
}
}
function autoCreateAnnotation() {
console.log('[校验插件] 自动批注流程开始');
function findLeafByText(text) {
let allEls = document.querySelectorAll('*');
for (let i = allEls.length - 1; i >= 0; i--) {
let el = allEls[i];
if (el.children.length === 0 && el.textContent.trim() === text && el.offsetWidth > 0 && el.offsetHeight > 0) {
return el;
}
}
return null;
}
function findParentClickable(el) {
let cur = el;
for (let i = 0; i < 5; i++) {
if (!cur) break;
if (cur.onclick || cur.tagName === 'BUTTON' || cur.getAttribute('role') === 'button' || (cur.className && typeof cur.className === 'string' && (cur.className.includes('btn') || cur.className.includes('item') || cur.className.includes('tool')))) {
return cur;
}
cur = cur.parentElement;
}
return el;
}
function clickElement(el) {
if (!el) return false;
let target = findParentClickable(el);
try { target.click(); } catch (e) {
target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
}
return true;
}
function checkModalVisible() {
let els = document.querySelectorAll('[class*="dialog"], [class*="modal"], [class*="popup"], [class*="popper"], [class*="drawer"]');
for (let el of els) {
if (el.offsetWidth > 0 && el.offsetHeight > 0 && el.textContent.length > 5) {
return el;
}
}
return null;
}
function findCreateBtn() {
let btns = document.querySelectorAll('button.el-button');
let visible = [];
btns.forEach(btn => {
let r = btn.getBoundingClientRect();
if (r.width > 0 && r.height > 0 && r.top < 100) visible.push(btn);
});
for (let btn of visible) {
let svg = btn.querySelector('svg path');
if (svg) {
let d = svg.getAttribute('d') || '';
if (d.startsWith('M17.185') || d.startsWith('M17 18') || d.includes('17.185')) {
return btn;
}
}
}
if (visible.length >= 3) return visible[2];
return null;
}
let step1 = findCreateBtn();
if (!step1) {
console.log('[校验插件] 未找到创建批注按钮');
return;
}
console.log('[校验插件] 步骤1: 点击创建批注按钮');
step1.click();
let modalRetries = 0;
setTimeout(() => {
// 首先检查弹窗是否已直接出现
let dialog = document.querySelector('.postil-dialog');
if (dialog && dialog.offsetWidth > 0) {
console.log('[校验插件] 弹窗直接出现,跳过步骤2');
proceedWithModal();
return;
}
console.log('[校验插件] 步骤2: 查找postil工具栏');
let step2 = null;
let step2Text = null;
let allEls = document.querySelectorAll('*');
for (let el of allEls) {
if (el.textContent.trim() === '指定标签批注' && el.offsetWidth > 0 && el.offsetHeight > 0) {
step2Text = el;
break;
}
}
if (step2Text) {
console.log('[校验插件] 步骤2: 点击指定标签批注文字');
clickElement(step2Text);
setTimeout(proceedWithModal, 800);
return;
}
let groups = document.querySelectorAll('.el-button-group.top-item, .top-item');
console.log('[校验插件] top-item容器数量:', groups.length);
for (let grp of groups) {
let btns = grp.querySelectorAll('button');
console.log('[校验插件] 容器内按钮数:', btns.length);
if (btns.length >= 2 && btns.length <= 4) {
step2 = btns[1];
console.log('[校验插件] 找到postil工具栏,点击第2个按钮(postilAssign)');
break;
}
}
if (!step2) {
console.log('[校验插件] 未找到top-item容器,尝试ysButton');
let ysBtns = document.querySelectorAll('[class*="button-theme-top"]');
console.log('[校验插件] button-theme-top数量:', ysBtns.length);
if (ysBtns.length >= 2) {
step2 = ysBtns[1];
console.log('[校验插件] 点击第2个ysButton');
}
}
if (!step2) {
console.log('[校验插件] 未找到postil工具栏');
return;
}
step2.click();
console.log('[校验插件] 步骤2: 已点击postilAssign按钮');
setTimeout(() => {
let modal = checkModalVisible();
if (modal) {
console.log('[校验插件] 弹窗已出现');
proceedWithModal();
} else {
console.log('[校验插件] 等待弹窗...');
setTimeout(() => {
if (checkModalVisible() || findLeafByText('类型')) {
console.log('[校验插件] 弹窗已出现(延迟)');
proceedWithModal();
} else {
console.log('[校验插件] 弹窗未出现');
}
}, 1000);
}
}, 1000);
}, 600);
function proceedWithModal() {
console.log('[校验插件] 步骤3: 选择"属性错误"并提交');
let dialog = document.querySelector('.postil-dialog');
if (!dialog || dialog.offsetWidth === 0) {
if (modalRetries < 10) {
modalRetries++;
console.log('[校验插件] 弹窗未出现,等待重试(' + modalRetries + '/10)...');
setTimeout(proceedWithModal, 500);
} else {
console.log('[校验插件] 弹窗等待超时,放弃操作');
modalRetries = 0;
}
return;
}
modalRetries = 0;
console.log('[校验插件] 弹窗已出现,开始操作');
let selectEl = dialog.querySelector('.postil-dialog-types');
if (!selectEl) {
console.log('[校验插件] 未找到.postil-dialog-types,尝试通用select');
selectEl = dialog.querySelector('.el-select');
if (!selectEl) {
console.log('[校验插件] 弹窗内未找到任何select元素');
return;
}
}
function findOption() {
let allItems = document.querySelectorAll('.el-select-dropdown__item');
for (let i = 0; i < allItems.length; i++) {
if (allItems[i].textContent.trim() === '属性错误') {
return allItems[i];
}
}
return null;
}
function submitAnnotation() {
setTimeout(() => {
let buttons = dialog.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
let text = buttons[i].textContent.trim();
if (text === '确定' && buttons[i].offsetWidth > 0) {
buttons[i].click();
console.log('[校验插件] 已点击确定按钮,批注流程完成');
return;
}
}
let allBtns = [];
for (let b of buttons) { if (b.offsetWidth > 0) allBtns.push(b.textContent.trim()); }
console.log('[校验插件] 未找到确定按钮,弹窗内按钮:', allBtns);
}, 300);
}
function tryOpenAndSelect() {
let option = findOption();
if (option) {
console.log('[校验插件] 找到"属性错误"选项,直接点击');
option.click();
submitAnnotation();
return true;
}
return false;
}
if (tryOpenAndSelect()) return;
console.log('[校验插件] 选项未找到,尝试打开下拉框');
let targets = [
selectEl.querySelector('.el-select__wrapper'),
selectEl.querySelector('.el-input__wrapper'),
selectEl.querySelector('input'),
selectEl
];
for (let t of targets) {
if (!t) continue;
console.log('[校验插件] 点击:', t.className || t.tagName);
try {
t.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, button: 0 }));
t.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, button: 0 }));
t.click();
} catch(e) {}
break;
}
setTimeout(() => {
if (tryOpenAndSelect()) return;
console.log('[校验插件] 第二次尝试打开下拉框');
for (let t of targets) {
if (!t) continue;
try {
t.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, button: 0 }));
t.click();
} catch(e) {}
break;
}
setTimeout(() => {
if (!tryOpenAndSelect()) {
let allItems = [];
document.querySelectorAll('.el-select-dropdown__item').forEach(item => {
allItems.push(item.textContent.trim());
});
console.log('[校验插件] 最终未找到"属性错误"选项,可用:', allItems);
}
}, 1000);
}, 800);
}
}
// ========== 8. 校验 ==========
function validate(isAuto) {
// 显示校验中状态
if (!panelReady) createPanel();
const content = document.getElementById('chk-content');
const btn = document.getElementById('chk-btn-check');
btn.textContent = '校验中...'; btn.disabled = true;
let statusBadge = document.getElementById('chk-status-badge');
if (statusBadge) { statusBadge.className = 'chk-status-badge chk-status-validating'; statusBadge.textContent = '校验中'; }
content.innerHTML = `
🔍 正在校验...
2D遮挡 + 3D可见性 + 3D遮挡
`;
setTimeout(function () {
let errors = [];
let info = [];
let result2D = check2DOcclusion();
if (result2D.views.length > 0) {
info.push(`2D遮挡校验(${result2D.views.join('/')})`);
}
errors = errors.concat(result2D.errors);
let result3D = check3DVisibility();
info.push(`3D可见性+遮挡校验(共${result3D.count}个)`);
for (let i = 0; i < result3D.missing.length; i++) {
let m = result3D.missing[i];
errors.push({ type: '3d', labelIndex: m.labelIndex, labelText: m.labelText, message: `[3D] ${m.message}` });
}
for (let i = 0; i < result3D.occlusionErrors.length; i++) {
let m = result3D.occlusionErrors[i];
errors.push({ type: '3d', labelIndex: m.labelIndex, labelText: m.labelText, message: `[3D] ${m.message}` });
}
renderResults(errors, info, isAuto);
}, 50);
}
// ========== 9. 切帧自动检测 ==========
let debounceTimer = null;
let pollTimer = null;
function getFrameKey() {
let labels = document.querySelectorAll('.threejs-label');
if (labels.length === 0) return '';
let parts = [];
for (let i = 0; i < Math.min(labels.length, 5); i++) {
parts.push((labels[i].textContent || '').substring(0, 50));
}
return labels.length + '|' + parts.join('||');
}
function doAutoValidate() {
if (isAutoValidating || !autoMode) return;
let frameKey = getFrameKey();
if (!frameKey || frameKey === lastFrameKey) return;
lastFrameKey = frameKey;
isAutoValidating = true;
clearCanvasText();
setTimeout(function () {
try {
validate(true);
} catch (e) {
console.log('[校验插件] 自动校验出错:', e.message);
}
setTimeout(function () {
isAutoValidating = false;
doAutoValidate();
}, 50);
}, 300);
}
let observer = new MutationObserver(function () {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(doAutoValidate, 300);
});
function startObserver() {
let target = document.getElementById('threejs-label') || document.body;
observer.observe(target, { childList: true, subtree: true, characterData: true });
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(doAutoValidate, 2000);
}
// ========== 10. 初始化 ==========
function init() {
let attempts = 0;
let interval = setInterval(function () {
attempts++;
if (document.getElementById('app') || attempts > 60) {
clearInterval(interval);
setTimeout(function () {
createPanel();
startObserver();
console.log('[校验插件V12.8] 已加载');
setTimeout(doAutoValidate, 1500);
}, 500);
}
}, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.shiftKey && e.key === 'V') {
e.preventDefault();
clearCanvasText();
validate(false);
}
});
window.__validate = validate;
window.__clearCanvasText = clearCanvasText;
window.__allCanvasTexts = allCanvasTexts;
window.__dump2D = function () {
let store = getVueStore();
if (!store || !store.state || !store.state.marks) { console.log('无Vue Store'); return; }
let marks = store.state.marks;
let frameIds = {};
for (let catKey in marks) {
let category = marks[catKey];
for (let instKey in category) {
let inst = category[instKey];
if (inst && inst['3d']) {
for (let fid in inst['3d']) frameIds[fid] = (frameIds[fid] || 0) + 1;
}
}
}
let curFrame = Object.keys(frameIds).sort(function (a, b) { return frameIds[b] - frameIds[a]; })[0];
console.log('当前帧:', curFrame, '帧统计:', frameIds);
let results = [];
for (let catKey in marks) {
let category = marks[catKey];
for (let instKey in category) {
let inst = category[instKey];
if (!inst || !inst['2d']) continue;
let d2 = inst['2d'];
let frame2d = d2[curFrame];
if (!frame2d) continue;
let boxList = [];
if (Array.isArray(frame2d)) boxList = frame2d;
else { for (let k in frame2d) { let v = frame2d[k]; if (Array.isArray(v)) boxList = boxList.concat(v); else if (typeof v === 'object' && v) boxList.push(v); } }
for (let i = 0; i < boxList.length; i++) {
let box = boxList[i];
if (!box) continue;
let cam = box.camera || '?';
let viewName = VIEW_NAMES[cam] || cam;
let attrs = box.attrs;
let occ = '无attrs';
if (attrs) {
if (typeof attrs === 'string') { try { attrs = JSON.parse(attrs); } catch (e) { } }
if (attrs.occlusion) {
occ = Array.isArray(attrs.occlusion) ? attrs.occlusion[0] : attrs.occlusion;
} else { occ = '无occlusion字段'; }
}
results.push({
view: viewName,
class: inst.class || instKey,
inst: instKey,
occlusion: occ,
hasAttrs: !!box.attrs,
allAttrKeys: box.attrs ? Object.keys(box.attrs) : []
});
}
}
}
console.table(results);
console.log('共' + results.length + '个2D框');
};
})();