// ==UserScript== // @name 西井 3D 标注点数校验(窗口内提示 + 点击定位) // @namespace local.xijing.dimcount // @version 6.0.0 // @description 按平台内部数据校验 3D 标注点数(point_num),小于阈值判异常;虚拟/预测(visual_type=2)标注归入跳过。点击异常项在点云中高亮标注框与标签。切帧自动校验,窗口可拖动可最小化。 // @author you // @match *://121.37.95.217:8080/pointcloud/* // @grant none // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const CONFIG = { threshold: 5, // 3D 标注点数小于该值判为异常 checkInterval: 250, // 检测间隔(毫秒) sound: false, // 出现异常时是否提示音 }; const VISUAL_VIRTUAL = 2; // 虚拟/预测(斜纹)——不校验 /* ============================ 数据读取 ============================ */ function getStore() { const app = (document.querySelector('#app') || {}).__vue_app__; if (!app || !app.config || !app.config.globalProperties) return null; try { return app.config.globalProperties.$store || null; } catch (e) { return null; } } function guessFrameId(store) { try { const marks = store.state.marks; if (!marks || typeof marks !== 'object') return null; const counts = {}; for (const key in marks) { const v = marks[key]; const d3 = v && v['3d']; if (d3 && typeof d3 === 'object') { for (const fid in d3) counts[fid] = (counts[fid] || 0) + 1; } } let best = null, max = -1; for (const fid in counts) { if (counts[fid] > max) { max = counts[fid]; best = fid; } } return best; } catch (e) { return null; } } function currentFrameId(store) { let id = store.state.status && store.state.status.curFrameId; if (id == null || id === '') id = guessFrameId(store); return id; } function resolveClassName(store, classId, toolType, fallback) { try { const getClassName = store.getters['config/className']; if (getClassName) { const name = getClassName(classId, toolType) || getClassName(classId); if (name) return name; } } catch (e) {} return fallback || String(classId); } function toRow(store, trackId, mark, getters, forceVirtual, classIdHint) { const getClassName = getters.getClassName; const getClassId = getters.getClassId; const count = mark.point_num != null ? +mark.point_num : 0; const vt = mark.visual_type != null ? +mark.visual_type : 0; const virtual = !!forceVirtual || vt === VISUAL_VIRTUAL; let classId = mark.classId != null ? mark.classId : classIdHint; if (classId == null && getClassId) { try { classId = getClassId(trackId); } catch (e) {} } const className = resolveClassName(store, classId, mark.type || 'volume', mark.class || trackId); return { trackId, classId, count, virtual, label: className + '-' + trackId, mark }; } // 返回 { rows, frameId };合并当前帧真实标注 + 虚拟/预测标注 function detect(store) { const frameId = currentFrameId(store); const rows = []; if (!frameId || !store.getters) return { rows, frameId }; const getters = { getClassName: store.getters['config/className'], getClassId: store.getters['marks/classId'], }; // 真实标注:平台官方 getter 整理好的当前帧数据 const fmGetter = store.getters['marks/frameMarks']; const frameMarks = (typeof fmGetter === 'function' && fmGetter(frameId)) || {}; const real3d = (frameMarks['3d']) || {}; for (const trackId in real3d) { const mark = real3d[trackId]; if (mark && typeof mark === 'object') rows.push(toRow(store, trackId, mark, getters, false, null)); } // 虚拟/预测标注存在 curVMarks[trackId]['3d'] const curVMarks = (store.state.marks && store.state.marks.curVMarks) || {}; for (const trackId in curVMarks) { const v = curVMarks[trackId]; const mark = v && v['3d']; if (mark && typeof mark === 'object') { rows.push(toRow(store, trackId, mark, getters, true, v.class)); } } return { rows, frameId }; } /* ============================ 界面 ============================ */ const CSS = ` #xjd3d-root{position:fixed;left:340px;top:80px;z-index:999999;font:13px/1.6 -apple-system,"Microsoft YaHei","PingFang SC",sans-serif;color:#dbeafe;user-select:none} #xjd3d-root *{box-sizing:border-box} #xjd3d-panel{position:relative;width:352px;max-height:72vh;max-width:calc(100vw - 40px);background:linear-gradient(155deg,rgba(13,20,38,.97),rgba(8,12,24,.97));backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(56,189,248,.28);border-radius:14px;box-shadow:0 14px 46px rgba(0,0,0,.62),0 0 0 1px rgba(56,189,248,.08),0 0 26px rgba(56,189,248,.12);overflow:hidden} #xjd3d-accent{position:relative;height:4px;background:linear-gradient(90deg,#22d3ee,#3b82f6,#818cf8);overflow:hidden} #xjd3d-accent::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.85),transparent);animation:xjd3d-shine 3.2s linear infinite} @keyframes xjd3d-shine{to{left:120%}} #xjd3d-head{position:relative;display:flex;align-items:center;justify-content:space-between;padding:11px 14px 9px;cursor:move;gap:10px;background:linear-gradient(180deg,rgba(56,189,248,.10),transparent)} #xjd3d-head:active{cursor:grabbing} #xjd3d-titlewrap{display:flex;align-items:center;gap:9px;min-width:0} #xjd3d-logo{width:27px;height:27px;border-radius:8px;background:linear-gradient(135deg,#22d3ee,#3b82f6);display:flex;align-items:center;justify-content:center;color:#04101f;font-size:13px;font-weight:800;flex:none;box-shadow:0 0 12px rgba(34,211,238,.55)} #xjd3d-led{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 9px #22c55e;flex:none;animation:xjd3d-led 1.6s ease-in-out infinite} #xjd3d-led.err{background:#ef4444;box-shadow:0 0 9px #ef4444} @keyframes xjd3d-led{0%,100%{opacity:1}50%{opacity:.35}} #xjd3d-title{font-size:14px;font-weight:700;color:#e6f7ff;letter-spacing:.5px;text-shadow:0 0 8px rgba(56,189,248,.35);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #xjd3d-sub{font-size:11px;color:#7aa0c0;font-weight:500;white-space:nowrap} #xjd3d-right{display:flex;align-items:center;gap:8px;cursor:default} #xjd3d-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap;border:1px solid transparent} #xjd3d-pill.ok{color:#4ade80;background:rgba(34,197,94,.12);border-color:rgba(34,197,94,.35);box-shadow:0 0 10px rgba(34,197,94,.15)} #xjd3d-pill.err{color:#f87171;background:rgba(239,68,68,.12);border-color:rgba(239,68,68,.4);box-shadow:0 0 10px rgba(239,68,68,.2)} #xjd3d-check{height:24px;padding:0 11px;border:none;border-radius:7px;background:linear-gradient(135deg,#06b6d4,#3b82f6);color:#04101f;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;transition:all .15s;box-shadow:0 0 12px rgba(56,189,248,.4)} #xjd3d-check:hover{transform:translateY(-1px);box-shadow:0 0 18px rgba(56,189,248,.6)} #xjd3d-check:active{transform:translateY(0)} #xjd3d-btn{width:24px;height:24px;border:1px solid rgba(56,189,248,.3);border-radius:7px;background:rgba(30,41,59,.6);color:#7aa0c0;cursor:pointer;font-size:15px;line-height:1;display:flex;align-items:center;justify-content:center;transition:all .15s} #xjd3d-btn:hover{background:rgba(56,189,248,.18);color:#e6f7ff;border-color:rgba(56,189,248,.6)} #xjd3d-body{position:relative;padding:4px 14px 14px;max-height:calc(72vh - 62px);overflow-y:auto} #xjd3d-body::-webkit-scrollbar{width:5px} #xjd3d-body::-webkit-scrollbar-track{background:transparent} #xjd3d-body::-webkit-scrollbar-thumb{background:rgba(56,189,248,.35);border-radius:4px} #xjd3d-body::-webkit-scrollbar-thumb:hover{background:rgba(56,189,248,.6)} #xjd3d-stats{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-bottom:12px} .xjd3d-stat{background:rgba(30,41,59,.5);border:1px solid rgba(56,189,248,.16);border-radius:11px;padding:9px 6px;text-align:center} .xjd3d-stat b{display:block;font-family:'JetBrains Mono','Consolas',monospace;font-size:20px;font-weight:700;line-height:1.2;color:#38bdf8;font-variant-numeric:tabular-nums;text-shadow:0 0 10px rgba(56,189,248,.55)} .xjd3d-stat span{display:block;font-size:11px;color:#7aa0c0;margin-top:2px} .xjd3d-stat.t-err b{color:#f87171;text-shadow:0 0 10px rgba(239,68,68,.55)} .xjd3d-stat.t-ok b{color:#4ade80;text-shadow:0 0 10px rgba(34,197,94,.5)} .xjd3d-stat.t-skip b{color:#7c8aa0;text-shadow:none} .xjd3d-warnhead{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:#f87171;margin:6px 2px 8px;text-shadow:0 0 8px rgba(239,68,68,.3)} .xjd3d-item{display:flex;align-items:center;justify-content:space-between;gap:10px;background:rgba(30,15,24,.5);border:1px solid rgba(239,68,68,.25);border-left:3px solid #f87171;border-radius:9px;padding:8px 10px;margin-bottom:7px;font-size:13px;color:#e2e8f0;cursor:pointer;transition:all .15s} .xjd3d-item:hover{background:rgba(239,68,68,.14);transform:translateX(2px);box-shadow:0 3px 12px rgba(239,68,68,.25)} .xjd3d-item .xc{font-family:'JetBrains Mono','Consolas',monospace;font-weight:700;color:#f87171;font-variant-numeric:tabular-nums;white-space:nowrap} .xjd3d-item .lbl{flex:1;word-break:break-all;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} .xjd3d-item .loc{font-size:10px;color:#67e8f9;background:rgba(6,182,212,.12);border:1px solid rgba(34,211,238,.3);border-radius:999px;padding:1px 8px;opacity:0;transition:opacity .15s;white-space:nowrap} .xjd3d-item:hover .loc{opacity:1} .xjd3d-item.located{border-left-color:#fbbf24;background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.4)} .xjd3d-item.located .loc{opacity:1;color:#fbbf24;background:rgba(245,158,11,.14);border-color:rgba(245,158,11,.4)} .xjd3d-pass{display:flex;align-items:center;justify-content:center;gap:8px;color:#4ade80;font-weight:600;font-size:14px;padding:24px 0;text-shadow:0 0 8px rgba(34,197,94,.35)} .xjd3d-pass .ic{width:26px;height:26px;border-radius:50%;background:rgba(34,197,94,.16);display:flex;align-items:center;justify-content:center;font-size:15px;box-shadow:0 0 10px rgba(34,197,94,.3)} .xjd3d-empty{color:#7aa0c0;text-align:center;padding:20px 0} .xjd3d-note{font-size:11px;color:#64748b;margin-top:8px;text-align:center} @keyframes xjd3d-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} #xjd3d-panel{animation:xjd3d-in .25s ease} `; let rootEl = null, bodyEl = null, pillEl = null, ledEl = null; let minimized = false; let currentRows = []; let lastSig = null; let lastFrameId = null; function buildUI() { if (document.getElementById('xjd3d-root')) return; const style = document.createElement('style'); style.textContent = CSS; document.head.appendChild(style); rootEl = document.createElement('div'); rootEl.id = 'xjd3d-root'; rootEl.innerHTML = `