// ==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 += `
⚠ 发现问题(${errors.length}条,点击可定位)
`; 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框'); }; })();