// ==UserScript==
// @name 标注平台辅助插件(奇偶帧/跨帧复制/视角过滤)
// @namespace https://github.com/local/od-annotation-helper
// @version 1.6.0
// @description 1) 按奇偶隐藏底部帧数标签,支持单帧显隐;2) O复制跨过被隐藏帧(向后两帧,带位姿/运动补偿),A/D翻页自动跳帧;3) 仅显示1、2视角(相机)图片上的2D框与伪3D框(默认不勾选,切换即时生效,无需刷新);4) 一键删除 3~7 视角的 2D 标注框数据(可当前帧或全部帧,删除后不重新生成,1、2视角与3D框保留)。
// @author Codex
// @match http://113.207.105.202:8080/pointcloud/*
// @run-at document-start
// @grant none
// @noframes
// ==/UserScript==
(function () {
'use strict';
/* ================================================================== *
* 配置
* ================================================================== */
const CONFIG = {
// 允许显示标注框的相机 key(视角 1、2)
allowedCameras: ['front_deep', 'front_mid'],
copyOffset: 2, // O 复制向后跨过的帧数
frameListSelector: '#pages',
frameItemSelector: '#pages > .page',
};
const STATE = {
parity: 'none', // none | odd(隐藏奇数) | even(隐藏偶数)
copySkip: true, // O 复制跨过被隐藏帧
onlyViews12: false, // 仅显示 1、2 视角标注框(默认关闭=显示全部,避免刷新后被自动勾选)
singleFrameMode: false, // 点击帧号=切换显隐
hideFrames: new Set(), // 强制隐藏的帧号
showFrames: new Set(), // 强制显示的帧号
panelBuilt: false,
minimized: false,
overridesInstalled: false,
};
const allowedSet = new Set(CONFIG.allowedCameras);
/* ================================================================== *
* 工具
* ================================================================== */
function waitFor(condition, timeout, interval) {
timeout = timeout == null ? 15000 : timeout;
interval = interval == null ? 60 : interval;
return new Promise(function (resolve) {
if (condition()) return resolve(true);
const start = Date.now();
const timer = setInterval(function () {
if (condition()) { clearInterval(timer); resolve(true); }
else if (Date.now() - start > timeout) { clearInterval(timer); resolve(false); }
}, interval);
});
}
function showToast(text) {
let host = document.getElementById('_od_helper_toast');
if (!host) {
host = document.createElement('div');
host.id = '_od_helper_toast';
host.style.cssText =
'position:fixed;top:72px;left:50%;transform:translateX(-50%);' +
'background:rgba(28,30,34,.96);color:#e8eaed;padding:8px 16px;border-radius:4px;border:1px solid #33363b;' +
'font-size:13px;z-index:2147483600;pointer-events:none;opacity:0;' +
'transition:opacity .2s;font-family:system-ui,"Microsoft YaHei",sans-serif;' +
'box-shadow:0 2px 12px rgba(0,0,0,.18);';
document.body.appendChild(host);
}
host.textContent = text;
host.style.opacity = '1';
clearTimeout(host._t);
host._t = setTimeout(function () { host.style.opacity = '0'; }, 2200);
}
// 帧号 = 帧标签在 #pages 中的位置 + 1
function getFrameNumberFromItem(el) {
const parent = el.parentNode;
if (!parent) return 0;
return Array.prototype.indexOf.call(parent.children, el) + 1;
}
function frameCount() {
const sm = window.instance && window.instance.StoreManager;
if (!sm) return 0;
const frames = sm.configState && sm.configState().frames;
return frames ? frames.length : 0;
}
function shouldHideFrame(num) {
if (STATE.showFrames.has(num)) return false;
if (STATE.hideFrames.has(num)) return true;
if (STATE.parity === 'odd') return num % 2 === 1; // 隐藏 1,3,5...
if (STATE.parity === 'even') return num % 2 === 0; // 隐藏 2,4,6...
return false;
}
/* ================================================================== *
* 功能 1:按奇偶 / 单帧隐藏底部帧数标签
* ================================================================== */
function paintFrames() {
const items = document.querySelectorAll(CONFIG.frameItemSelector);
if (!items.length) return;
for (let i = 0; i < items.length; i++) {
const el = items[i];
if (shouldHideFrame(i + 1)) el.style.setProperty('display', 'none', 'important');
else el.style.removeProperty('display');
}
}
let frameObserver = null;
let docFrameObserver = null;
let paintQueued = false;
function queuePaint() {
if (paintQueued) return;
paintQueued = true;
requestAnimationFrame(function () { paintQueued = false; paintFrames(); });
}
function attachFrameObserver(root) {
if (!root || frameObserver) return;
frameObserver = new MutationObserver(queuePaint);
frameObserver.observe(root, { childList: true });
queuePaint();
}
function observeFrames() {
const root = document.querySelector(CONFIG.frameListSelector);
if (root) { attachFrameObserver(root); return; }
if (!docFrameObserver) {
docFrameObserver = new MutationObserver(function () {
const r = document.querySelector(CONFIG.frameListSelector);
if (r && !frameObserver) {
attachFrameObserver(r);
docFrameObserver.disconnect();
docFrameObserver = null;
}
});
docFrameObserver.observe(document, { childList: true, subtree: true });
}
}
if (window.MutationObserver) observeFrames();
else setInterval(paintFrames, 800);
// 点击帧号:单帧显隐模式 / 平台默认跳帧
document.addEventListener('click', function (ev) {
const el = ev.target.closest && ev.target.closest(CONFIG.frameItemSelector);
if (!el) return;
if (!STATE.singleFrameMode) return;
const num = getFrameNumberFromItem(el);
if (STATE.hideFrames.has(num)) {
STATE.hideFrames.delete(num);
STATE.showFrames.add(num);
showToast('已显示第 ' + num + ' 帧');
} else {
STATE.showFrames.delete(num);
STATE.hideFrames.add(num);
showToast('已隐藏第 ' + num + ' 帧');
}
ev.preventDefault();
ev.stopPropagation();
queuePaint();
}, true);
/* ================================================================== *
* A/D 翻页:奇偶隐藏生效时自动跳到下一个可见帧
* ================================================================== */
function findNextVisibleIndex(startIdx, dir) {
const total = frameCount();
if (!total) return -1;
let i = startIdx;
for (;;) {
i += dir;
if (i < 0 || i >= total) return -1;
if (!shouldHideFrame(i + 1)) return i;
}
}
document.addEventListener('keydown', function (ev) {
if (STATE.parity === 'none' || STATE.singleFrameMode) return;
const k = ev.key && ev.key.toLowerCase();
if (k !== 'a' && k !== 'd') return;
if (ev.ctrlKey || ev.altKey || ev.metaKey) return;
const t = ev.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
const sm = window.instance && window.instance.StoreManager;
if (!sm) return;
const cur = sm.getCurFrameId();
const idx = sm.configGetter('getFrameIndex')(cur);
const nextIdx = findNextVisibleIndex(idx, k === 'd' ? 1 : -1);
ev.preventDefault();
ev.stopPropagation();
if (nextIdx === -1 || nextIdx === idx) return;
const tid = sm.configGetter('getFrameId')(nextIdx);
if (tid !== -1) sm.statusCommit('setCurFrameId', tid);
}, true);
/* ================================================================== *
* 功能 2:O 复制 → 跨过被隐藏帧(向后两帧,带位姿/运动补偿)
* ================================================================== */
function installCopyOverride(sm) {
if (sm.__odCopyPatched) return;
sm.__odCopyPatched = true;
sm.copyAllMarks = function () {
if (this.statusState().copyStartFrameId || this.statusState().selectMarkInfo.trackId) {
this.statusCommit('updateProperty', ['copyStartFrameId', null]);
this.statusCommit('selectMarkInfo', [null, null, null, null, null]);
}
const offset = STATE.copySkip && STATE.parity !== 'none' ? CONFIG.copyOffset : 1;
const e = this.getCurFrameId();
const n = this.configGetter('getFrameIndex')(e);
const r = this.configGetter('getFrameId')(n + offset);
if (r == -1) { showToast('已处于最后一帧,无法复制!'); return; }
let hasMarks = false;
const marks = this.marksState().marks;
for (const trackId in marks) {
const m3 = marks[trackId]['3d'];
const m2 = marks[trackId]['2d'];
if (!((!m3 || !m3[e]) && (!m2 || !m2[e]))) { hasMarks = true; break; }
}
if (!hasMarks) { showToast('当前帧无可复制标注物!'); return; }
const content = offset === 1
? '是否复制当前帧所有标注物到下一帧?'
: '是否复制当前帧所有标注物到向后 ' + offset + ' 帧的帧(跳过被隐藏帧)?';
const dialog = window.instance && window.instance.showTipsDialog
? window.instance.showTipsDialog : null;
if (dialog) {
dialog({
content: content,
btnTitles: ['取消', '确定'],
callbacks: [null, function () { this.startCopyTask(e, r, true); }.bind(this)],
});
} else {
this.startCopyTask(e, r, true);
}
};
}
/* ================================================================== *
* 功能 3:仅显示 1、2 视角标注框(显示层过滤,不动数据)
* ================================================================== */
function installViewFilter(sm, ft, store) {
// 3.1 伪3D框投影:compute2DProjection 的结果仅保留白名单相机
if (ft && !ft.__odProjectPatched) {
ft.__odProjectPatched = true;
const orig = ft.compute2DProjection.bind(ft);
ft.compute2DProjection = function (mark, trackId, classId, frameId, updateEvent) {
const res = orig(mark, trackId, classId, frameId, updateEvent);
if (!STATE.onlyViews12 || !res) return res;
const out = {};
for (const key in res) if (allowedSet.has(key)) out[key] = res[key];
return out;
};
}
// 3.2 2D框:替换 curFrameMarks 为“每次访问都过滤”的 getter,避免 Vue computed 缓存旧值
// (消费方都只读 .value,不依赖响应式,安全)
if (sm && !sm.__odCurMarksPatched) {
sm.__odCurMarksPatched = true;
const origFrameMarks = sm.frameMarks.bind(sm);
Object.defineProperty(sm, 'curFrameMarks', {
configurable: true,
enumerable: true,
get: function () {
const self = this;
return {
get value() {
const fm = origFrameMarks.call(self);
if (!STATE.onlyViews12 || !fm || !fm['2d']) return fm;
const filtered = {};
for (const trackId in fm['2d']) {
const arr = (fm['2d'][trackId] || []).filter(function (m) {
return m && allowedSet.has(m.camera);
});
if (arr.length) filtered[trackId] = arr;
}
return { '3d': fm['3d'] || {}, '2d': filtered };
}
};
}
});
}
// 3.2b 可见性级过滤:拦截 marksGetter('isVisible'),让非白名单相机的 2D 框/投影
// 在上屏或重算可见性时直接判为不可见(仅显示层隐藏,底层数据不动)。
// 这样切换开关后,只要触发一次平台原生重绘(updateMarksVisible + erase/paint),
// 即可即时隐藏/恢复,无需刷新页面。
if (sm && !sm.__odIsVisiblePatched) {
sm.__odIsVisiblePatched = true;
const origMarksGetter = sm.marksGetter.bind(sm);
let wrappedIsVisible = null;
let wrappedGetMarks = null;
const wrapperFn = function (name) {
const fn = origMarksGetter(name);
if (name === 'isVisible') {
if (!wrappedIsVisible) {
wrappedIsVisible = function (isPostil, mark, trackId, classId) {
if (STATE.onlyViews12 && !isPostil && mark && mark.camera && !allowedSet.has(mark.camera)) {
return false;
}
return fn(isPostil, mark, trackId, classId);
};
}
return wrappedIsVisible;
}
if (name === 'getMarks') {
if (!wrappedGetMarks) {
wrappedGetMarks = function (trackId, dimension, frameId, camera) {
const res = fn(trackId, dimension, frameId, camera);
// 仅当读取“某一帧的全部 2D 框”(未指定 camera)且开启视角过滤时,
// 才只保留白名单相机的框;带 camera 的调用(如投影重算)保持原样,避免误删。
if (!STATE.onlyViews12 || dimension !== '2d' || camera) return res;
if (Array.isArray(res)) {
return res.filter(function (m) { return m && allowedSet.has(m.camera); });
}
return res;
};
}
return wrappedGetMarks;
}
return fn;
};
try { sm.marksGetter = wrapperFn; }
catch (e) {
try {
Object.defineProperty(sm, 'marksGetter', {
configurable: true,
get: function () { return wrapperFn; },
});
} catch (e2) {}
}
}
// 3.3 编辑/新增 2D 标注时,BUS_ADD_MODIFY_2D 会绕过 curFrameMarks 直接上屏;
// 通过拦截 commit,把非白名单相机的“上屏事件”抑制掉(数据仍写入 store,仅不上屏)。
// 注意:真正启动时 store 可能还未被 setStore,必须等真实 Vuex store 出现后再装,
// 否则 getStore() 会返回 useStore 函数,覆盖它的 commit 会报错。
installCommitOverride(store);
}
function isRealStore(store) {
return !!(store && typeof store.commit === 'function' && store._modules);
}
function installCommitOverride(store) {
if (!isRealStore(store) || store.__odCommitPatched) return;
store.__odCommitPatched = true;
const origCommit = store.commit.bind(store);
store.commit = function (type, payload) {
if (STATE.onlyViews12 && type === 'marks/addOrReplaceMark2D' && Array.isArray(payload)) {
const mark = payload[0];
if (mark && mark.camera && !allowedSet.has(mark.camera)) {
payload = payload.slice();
payload[5] = false; // 关闭该 mutation 的事件上屏,数据照常写入
}
}
return origCommit(type, payload);
};
}
/* ================================================================== *
* 面板 UI(匹配标注界面深色主题,可拖动、可最小化)
* ================================================================== */
const PALETTE = {
primary: '#7c5cff', primaryHover: '#9a7fff', primaryActive: '#6a4bd8',
textMain: '#e8eaed', textRegular: '#c9cdd2', textSecondary: '#9aa0a6',
border: '#2a3145', borderLight: '#252c3d', bg: '#141826', bgLight: '#1a2030',
hover: '#2a2d31', success: '#67c23a', info: '#909399',
};
function buildPanel() {
if (STATE.panelBuilt || !document.body) return;
STATE.panelBuilt = true;
const panel = document.createElement('div');
panel.id = '_od_helper_panel';
panel.className = 'od-panel';
panel.style.cssText =
'position:fixed;top:52px;right:12px;width:320px;z-index:2147483400;' +
'font-family:system-ui,"Microsoft YaHei",sans-serif;font-size:13px;color:#e8eaed;' +
'user-select:none;overflow:hidden;';
const header = document.createElement('div');
header.className = 'od-head';
header.innerHTML =
'标注平台辅助' +
'' +
'' +
'' +
'';
const body = document.createElement('div');
body.className = '_od_helper_body od-body';
body.innerHTML =
'
' +
'
帧数显示
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'
' +
'' +
'
复制 / 翻页
' +
'
' +
'
' +
'' +
'
视角标注
' +
'
' +
'
' +
'' +
'
数据清理
' +
'
' +
'
' +
'
' +
'';
panel.appendChild(header);
panel.appendChild(body);
document.body.appendChild(panel);
const css = document.createElement('style');
css.id = '_od_helper_style';
css.textContent =
'#_od_helper_panel{background:linear-gradient(180deg,#171c28,#10141d);border:1px solid #232a38;' +
'border-radius:10px;box-shadow:0 12px 32px rgba(0,0,0,.5),0 0 0 1px rgba(124,92,255,.05);}' +
'#_od_helper_panel .od-head{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 12px;' +
'background:linear-gradient(180deg,#1b2130,#141826);border-bottom:1px solid #232a38;cursor:move;position:relative;}' +
'#_od_helper_panel .od-head::after{content:\'\';position:absolute;left:0;right:0;bottom:-1px;height:1px;' +
'background:linear-gradient(90deg,transparent,rgba(124,92,255,.30),transparent);}' +
'#_od_helper_panel .od-head-t{display:flex;align-items:center;gap:8px;min-width:0;}' +
'#_od_helper_panel .od-title{font-weight:600;font-size:13px;color:#e3e6ee;letter-spacing:.4px;white-space:nowrap;}' +
'#_od_helper_panel .od-dot{width:9px;height:9px;flex:0 0 auto;border-radius:50%;background:#7f6fe0;' +
'animation:odBreath 2.6s ease-in-out infinite;}' +
'@keyframes odBreath{0%,100%{box-shadow:0 0 3px rgba(127,111,224,.35),0 0 8px rgba(127,111,224,.2);opacity:.75;}' +
'50%{box-shadow:0 0 6px rgba(127,111,224,.7),0 0 14px rgba(127,111,224,.4);opacity:1;}}' +
'#_od_helper_panel .od-head-btns{display:flex;gap:2px;flex:0 0 auto;}' +
'#_od_helper_panel .od-hbtn{width:24px;height:24px;line-height:1;padding:0;border:none;background:transparent;' +
'color:#6b7280;font-size:15px;cursor:pointer;border-radius:5px;transition:all .15s;}' +
'#_od_helper_panel .od-hbtn:hover{background:#242b3a;color:#e3e6ee;}' +
'#_od_helper_panel .od-body{padding:2px 14px 14px;}' +
'#_od_helper_panel .od-sec{padding-top:14px;}' +
'#_od_helper_panel .od-sec+.od-sec{margin-top:9px;padding-top:14px;border-top:1px solid #232a38;}' +
'#_od_helper_panel .od-sec-title{display:flex;align-items:center;gap:7px;margin-bottom:10px;font-weight:600;' +
'font-size:12.5px;color:#e3e6ee;letter-spacing:.4px;white-space:nowrap;}' +
'#_od_helper_panel .od-bar{width:3px;height:13px;flex:0 0 auto;border-radius:2px;background:#8a7cf0;}' +
'#_od_helper_panel .od-bar-danger{background:#f06473;}' +
'#_od_helper_panel .od-seg{display:flex;gap:7px;margin-bottom:10px;}' +
'#_od_helper_panel .od-seg-btn{flex:1;border:1px solid #262d3c;background:#161a24;' +
'border-radius:6px;padding:6px 0;cursor:pointer;font-size:12px;color:#a8aebd;transition:all .15s;text-align:center;letter-spacing:.3px;white-space:nowrap;}' +
'#_od_helper_panel .od-seg-btn:hover{border-color:#8a7cf0;color:#bcb0f2;background:#1b2130;}' +
'#_od_helper_panel .od-seg-btn.active{background:#6b5bd0;border-color:#7c6ce0;color:#fff;font-weight:600;' +
'box-shadow:0 1px 3px rgba(0,0,0,.35);}' +
'#_od_helper_panel .od-seg-btn:active{transform:scale(.97);}' +
'#_od_helper_panel .od-check{display:flex;align-items:center;gap:8px;cursor:pointer;color:#b6bcd0;font-size:12.5px;line-height:1.4;}' +
'#_od_helper_panel .od-check:hover{color:#e3e6ee;}' +
'#_od_helper_panel .od-check input[type=checkbox]{width:14px;height:14px;margin:0;flex:0 0 auto;accent-color:#7c5cff;cursor:pointer;}' +
'#_od_helper_panel .od-del-btn{display:flex;align-items:center;justify-content:center;gap:7px;width:100%;margin-top:10px;' +
'border:1px solid #3f2a31;background:#1d181c;border-radius:6px;padding:8px 0;cursor:pointer;' +
'font-size:12.5px;color:#ff8593;font-weight:600;transition:all .18s;letter-spacing:.3px;}' +
'#_od_helper_panel .od-del-btn:hover{border-color:#e85a6b;color:#ff9aa6;background:#241c1f;}' +
'#_od_helper_panel .od-del-btn:active{transform:scale(.98);}' +
'#_od_helper_panel .od-del-ico{font-size:14px;}' +
'#_od_helper_panel .od-foot{margin-top:14px;padding-top:10px;border-top:1px solid #232a38;text-align:center;' +
'font-size:10.5px;color:#5c6470;letter-spacing:1px;}' +
'#_od_helper_panel .od-foot-dot{margin-left:6px;color:#7f6fe0;animation:odBreath 3s ease-in-out infinite;}';
document.head.appendChild(css);
// 事件
header.querySelector('[data-od="min"]').addEventListener('click', function () {
STATE.minimized = !STATE.minimized;
body.style.display = STATE.minimized ? 'none' : 'block';
panel.style.width = STATE.minimized ? 'auto' : '270px';
});
header.querySelector('[data-od="close"]').addEventListener('click', function () {
panel.style.display = 'none';
let btn = document.getElementById('_od_helper_open');
if (!btn) {
btn = document.createElement('button');
btn.id = '_od_helper_open';
btn.textContent = '标注辅助';
btn.style.cssText =
'position:fixed;top:52px;right:12px;z-index:2147483400;padding:6px 12px;' +
'background:' + PALETTE.primary + ';border:none;border-radius:4px;color:#fff;cursor:pointer;' +
'font-size:12px;font-family:system-ui,"Microsoft YaHei",sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.15);';
btn.addEventListener('click', function () {
btn.style.display = 'none';
panel.style.display = 'block';
});
document.body.appendChild(btn);
} else btn.style.display = 'block';
});
body.querySelectorAll('[data-parity]').forEach(function (b) {
b.addEventListener('click', function () { setParity(b.getAttribute('data-parity')); });
});
body.querySelector('[data-od="single"]').addEventListener('change', function (e) {
STATE.singleFrameMode = e.target.checked;
});
body.querySelector('[data-od="copy"]').addEventListener('change', function (e) {
STATE.copySkip = e.target.checked;
});
body.querySelector('[data-od="views"]').addEventListener('change', function (e) {
STATE.onlyViews12 = e.target.checked;
queuePaint();
refreshViewFilter();
showToast(STATE.onlyViews12 ? '已启用:仅显示 1、2 视角标注' : '已关闭视角过滤');
});
body.querySelector('[data-od="delviews"]').addEventListener('click', confirmDeleteViewsBeyond12);
// 拖动
let drag = null;
header.addEventListener('mousedown', function (e) {
if (e.target.closest('button')) return;
const rect = panel.getBoundingClientRect();
drag = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!drag) return;
panel.style.left = (e.clientX - drag.dx) + 'px';
panel.style.top = (e.clientY - drag.dy) + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', function () { drag = null; });
syncParityButtons();
}
function setParity(mode) {
STATE.parity = mode;
STATE.hideFrames.clear();
STATE.showFrames.clear();
syncParityButtons();
queuePaint();
showToast(mode === 'none' ? '已恢复显示全部帧'
: (mode === 'odd' ? '已隐藏奇数帧,A/D 自动跳帧' : '已隐藏偶数帧,A/D 自动跳帧'));
}
function syncParityButtons() {
document.querySelectorAll('#_od_helper_panel [data-parity]').forEach(function (b) {
b.classList.toggle('active', STATE.parity === b.getAttribute('data-parity'));
});
}
// —— 相机视图即时显隐(无需刷新)——
// 关键:相机容器组件把 cameraKey→视图 的映射存在 setup() 的闭包变量里(不在 setupState 上),
// 所以无法从外部直接拿到相机视图。但每个 CameraView 实例都在构造时 watch 了
// statusState().isHidePostil / markVisible,一旦这些状态变化,所有相机视图都会
// 执行 updateMarksVisible() + erasePaint()(即平台原生"过滤变化"的重绘路径)。
// 因此这里用 statusCommit 短暂触发 isHidePostil 来强制所有相机视图重算可见性并重绘,
// 配合 marksGetter('isVisible') 覆写,即可让"仅显示1、2视角"开关即时生效。
function triggerCameraRepaint() {
const sm = window.instance && window.instance.StoreManager;
if (!sm) return;
try {
const cur = sm.statusState().isHidePostil;
// 切到相反值:每个 CameraView 的 watch 触发 → updateMarksVisible + erasePaint
sm.statusCommit('updateProperty', ['isHidePostil', cur ? 0 : 1]);
// 待 watcher flush 后再恢复原值,保持用户设置不变(约一个渲染帧)
setTimeout(function () {
sm.statusCommit('updateProperty', ['isHidePostil', cur]);
}, 80);
} catch (e) {}
}
// 切换视角过滤后,立即让所有相机视图重算可见性并重绘
function refreshViewFilter() {
try { triggerCameraRepaint(); } catch (e) {}
// 兜底:顺便刷新 3D 视图,确保点云侧同步
const viewer = window.instance && window.instance.viewer;
if (viewer) { try { viewer.rendererMain && viewer.rendererMain(); } catch (e) {} }
}
/* ================================================================== *
* 功能 4:一键删除 3~7 视角标注框(真删数据)
* ================================================================== */
// 平台把 3D 框投影到各相机并持久化为 2D 框 marks[trackId]['2d'][frameId](每条带 .camera)。
// 这里用 markCommit('deleteMarks', [trackId, frameId, '2d', camera, true]) 删除非白名单相机的 2D 框;
// 第 5 参 true 会把该相机写入 no_maps,避免下次重绘被重新生成(与平台 Delete 键行为一致)。
function collectDeleteTargets(allFrames) {
const sm = window.instance && window.instance.StoreManager;
const targets = [];
if (!sm) return targets;
const marks = sm.marksState && sm.marksState().marks;
if (!marks) return targets;
const mk = function (t, f, c) { return { trackId: t, frameId: f, camera: c }; };
const seen = new Set();
// 帧号键可能是 number 或 string,做一次容错匹配
const pick = function (m2, frameId) {
if (!m2) return null;
if (m2[frameId]) return m2[frameId];
if (m2[String(frameId)]) return m2[String(frameId)];
return m2[Number(frameId)] || null;
};
if (allFrames) {
for (const trackId in marks) {
const m2 = marks[trackId] && marks[trackId]['2d'];
if (!m2) continue;
for (const frameId in m2) {
const arr = pick(m2, frameId);
if (!arr) continue;
for (const m of arr) {
if (m && m.camera && !allowedSet.has(m.camera)) {
const key = trackId + '|' + frameId + '|' + m.camera;
if (!seen.has(key)) { seen.add(key); targets.push(mk(trackId, frameId, m.camera)); }
}
}
}
}
} else {
const frameId = sm.getCurFrameId();
for (const trackId in marks) {
const m2 = marks[trackId] && marks[trackId]['2d'];
if (!m2) continue;
const arr = pick(m2, frameId);
if (!arr) continue;
for (const m of arr) {
if (m && m.camera && !allowedSet.has(m.camera)) {
const key = trackId + '|' + m.camera;
if (!seen.has(key)) { seen.add(key); targets.push(mk(trackId, frameId, m.camera)); }
}
}
}
}
return targets;
}
function runDeleteViewsBeyond12(targets, allFrames) {
const sm = window.instance && window.instance.StoreManager;
if (!sm || !targets.length) return;
let deleted = 0;
for (const t of targets) {
try {
sm.markCommit('deleteMarks', [t.trackId, t.frameId, '2d', t.camera, true]);
deleted++;
} catch (e) {}
}
console.info('[OD Helper] 删除 3~7 视角 2D 框:共 ' + targets.length + ' 个,成功 ' + deleted + ' 个。', targets);
refreshViewFilter();
showToast(allFrames
? '已删除 ' + deleted + '/' + targets.length + ' 个 3~7 视角标注框(全部帧)'
: '当前帧已删除 ' + deleted + '/' + targets.length + ' 个 3~7 视角标注框');
}
function confirmDeleteViewsBeyond12() {
const sm = window.instance && window.instance.StoreManager;
if (!sm) { showToast('插件尚未就绪'); return; }
if (window.instance && window.instance.canEdit === false) {
showToast('当前为只读任务,无法删除标注数据');
return;
}
const allBox = document.getElementById('_od_helper_panel') &&
document.querySelector('#_od_helper_panel [data-od="delall"]');
const allFrames = !!(allBox && allBox.checked);
const targets = collectDeleteTargets(allFrames);
if (!targets.length) {
showToast(allFrames ? '没有 3~7 视角的标注框可删除' : '当前帧没有 3~7 视角的标注框');
return;
}
const frameId = sm.getCurFrameId();
const content = '将删除 ' + (allFrames ? '全部帧' : '当前帧 ' + frameId) + ' 共 ' + targets.length +
' 个 3~7 视角的 2D 标注框。\n仅影响当前任务的数据,不影响其它项目;1、2 视角与 3D 框数据保留。\n该操作不可恢复,是否继续?';
const dialog = window.instance && window.instance.showTipsDialog ? window.instance.showTipsDialog : null;
if (dialog) {
dialog({
content: content,
btnTitles: ['取消', '确定'],
callbacks: [null, function () { runDeleteViewsBeyond12(targets, allFrames); }],
});
} else if (window.confirm(content)) {
runDeleteViewsBeyond12(targets, allFrames);
}
}
/* ================================================================== *
* 启动
* ================================================================== */
function installOverrides() {
if (STATE.overridesInstalled) return;
const sm = window.instance && window.instance.StoreManager;
const ft = window.instance && window.instance.fusionTools;
if (!sm || !ft) return;
installCopyOverride(sm);
installViewFilter(sm, ft, null);
STATE.overridesInstalled = true;
console.info('[OD Helper] 标注平台辅助插件已启用。');
// store 可能尚未初始化,等真实 Vuex store 出现后再装 commit 拦截
waitFor(function () {
try { return isRealStore(sm.getStore()); } catch (e) { return false; }
}).then(function () {
try { installCommitOverride(sm.getStore()); } catch (e) {}
});
}
function boot() {
// 尽早安装覆写,确保在第一次相机渲染前生效(避免 Vue computed 缓存旧数据)
waitFor(function () {
return !!(window.instance && window.instance.StoreManager && window.instance.fusionTools);
}).then(installOverrides);
// 面板相关在 DOM 就绪后再构建
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', buildPanel);
} else {
buildPanel();
}
}
boot();
})();