源码安装
zhangsan / xieying3D_rollback

// ==UserScript==
// @name         xieying3D_rollback
// @namespace    http://tampermonkey.net/
// @version      2025.2.14.1
// @description  try it
// @author       You
// @match        https://data-encoder.ruqimobility.com/tool/pc?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ruqimobility.com
// @grant        none
// @license     MIT
// ==/UserScript==
window.$ = Document.prototype.$ = Element.prototype.$ = $;
window.$$ = Document.prototype.$$ = Element.prototype.$$ = $$;

const _window = window
window.cccallback = []
window._ds = {
    logAn: false,
    logKeydown: false,
    isDebug: true,
    objSum: 0,
    pointSize: 0.17,
    callback: [],
    viewBtnMap: {},
}

const _ds = new Proxy(window._ds, {
    get(target, prop) {
        return Reflect.get(target, prop)
    },
    set(target, prop, value) {
        if(prop in trigger) {
            trigger[prop](value)
        } else {
            Reflect.set(target, prop, value)
        }
        return true
    }

})

const trigger = {
    resultDir(newVal) {
        window._ds.resultDir = newVal
    },
    taskId(newVal) {
        window._ds.taskId = newVal

        const logTitle = $('.log-title')
        logTitle.style.cursor = 'pointer'
        logTitle.onclick = function() {
            copyToClipboard(newVal).then(res => showMessage('复制:任务id', {type: 'success'}))
        }
        logTitle.innerHTML = logTitle.textContent + ' '.repeat(2) + newVal
    }
};



hijackXHR(function() {
    const xhr = this;
    let taskId
    if(taskId = new RegExp(`^https://data-encoder.ruqimobility.com/annotation/dataset/info/(\\d*)`).exec(xhr.responseURL)?.[1]) {
        document.title = taskId
        _ds.taskId = taskId
    }
});


const realAEL = EventTarget.prototype.addEventListener;
const realREL = EventTarget.prototype.removeEventListener;
const listenerMap = new WeakMap(); // 存储原始监听器和包装后的监听器的映射关系

EventTarget.prototype.addEventListener = function(type, listener, options) {
    const wrappedListener = function(e) {
        if(type == 'keydown' && [81, 87, 69, 65, 83, 68].includes(e.keyCode) && !e.ctrlKey) { // Q81 W87 E69 A65 S83 D68
            // ;['keydown', 'keyup'].forEach((event) => {
            //     document.body.dispatchEvent(new KeyboardEvent(event, {
            //         code: "KeyZ",
            //         key: "z",
            //         keyCode: 90,
            //         ctrlKey: false,
            //         bubbles: true
            //     }))
            // });
            if(listener.name !== 'keydownCallback') return
        }

        if(type == 'keydown' && e.keyCode == 32 && listener.name !== 'keydownCallback') return

        if(type == 'keydown' && e.keyCode >= 49 && e.keyCode <= 53) {
            if(e.currentTarget == document.body && e.target == document.body) {
                $$('.label-container2 .select-label').find(label => label.textContent === ['小车', 'SUV', '两轮车', '人', '隔离柱'][e.keyCode-49]).click()
            }
            return
        }

        listener.apply(this, arguments);
    };

    if (!listenerMap.has(this)) {
        listenerMap.set(this, new Map());
    }
    listenerMap.get(this).set(listener, wrappedListener);

    realAEL.call(this, type, wrappedListener, options);

};

EventTarget.prototype.removeEventListener = function(type, listener, options) {
    const wrappedListener = listenerMap.get(this)?.get(listener);

    if (wrappedListener) {
        realREL.call(this, type, wrappedListener, options);

        listenerMap.get(this).delete(listener);
    }
};


;(function() {
    document.addEventListener('contextmenu',function(e){
        e.preventDefault();
    })
})();

let frameQueue = ['caret-right', 'caret-left']
document.body.addEventListener('keyup', function(e) {
    if([81, 87, 69, 65, 83, 68].includes(e.keyCode)) {
        document.body.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}))
    }
})
document.body.addEventListener('keydown', function keydownCallback(e) {
    const { keyCode } = e
    _ds.logKeydown && console.log(keyCode, e)

    const isInput = ['input', 'textarea'].find(sel => {
        const activeEl = document.activeElement
        if(!activeEl?.matches(sel)) return
        if(sel == 'input' && activeEl.type !== 'text') return false
        return true
    })
    if(isInput) return;

    [
        [
            () => [81, 87, 69, 65, 83, 68].includes(keyCode),
            () => {
                e.preventDefault();

                const triggerBtn = _ds.viewBtnMap[keyCode];
                if(!triggerBtn) return;

                triggerBtn.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}))
                if(e.altKey) document.body.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}))

            }
        ],
        [
            () => keyCode == 89,
            () => {
                setTimeout(() => $$('.item-warp-li').find(item => item.children[0].matches('.item.active'))?.click())
            }
        ],
        [
            () => [9, 32].includes(keyCode), // 【Tab】
            (e) => {
                e.preventDefault()

                if($('.main-view').style.display !== 'none') {
                    $(`span[aria-label="${keyCode == 9 ? 'caret-left' : 'caret-right'}"]`).parentElement.click()
                } else {
                    $$('.page-turn-btn button').find(btn => btn.textContent == ` ${keyCode == 9 ? '上一页' : '下一页'} `).click()
                }
            }
        ],
        [
            () => keyCode == 192,
            () => {
                // showMessage('反转:Tap切帧方向')
                // frameQueue.reverse()
                const btns = $$('label').filter(item => ['单帧视图', '多帧视图'].some(text => item.textContent == text))
                btns.find(btn => !btn.className.includes('checked'))?.click()
            }
        ],
    ].forEach((item) => { item[0]() && item[1](e) })
})

const viewScheme = { //Q81 W87 E69 A65 S83 D68
    '俯视图': {
        81: 0,
        87: 6,
        69: 1,
        65: 9,
        83: 3,
        68: 10,
    },
    '侧视图': {
        81: 7,
        87: 4,
        69: 2,
        65: 3,
        83: 5,
        68: 6,
    },
    '后视图': {
        81: 11,
        87: 4,
        69: 8,
        65: 9,
        83: 5,
        68: 10,
    }
}

const appear = {
    imgView: false,
    maxView: false,
}
let roundCount = 1
Obs(document.body, mrs => {
    mrs.forEach(mr => {
        // console.log(mr)
        [...mr.addedNodes].some(an => {
            _ds.logAn && console.log(roundCount, an);

            function bindView(viewWrap) { //键盘调整三视图
                const btnItems = viewWrap.$$('.item')

                viewWrap.$$('.side-view').forEach(item => {
                    item.addEventListener('mouseenter', (e) => {
                        const angle_view = item.$('.title1').textContent
                        const scheme = viewScheme[angle_view]

                        const viewBtnMap = {}
                        for(let keyCode in scheme) {
                            viewBtnMap[keyCode] = btnItems[scheme[keyCode]]
                        }
                        _ds.viewBtnMap = viewBtnMap
                    })

                    item.addEventListener('mouseleave', (e) => {
                        _ds.viewBtnMap = {}
                    })
                })
            }

            function set2DImgPositionInfo(cameraNumEl) {
                const imgPosition = ['后', '前远', '前广', '左后', '左前', '右后', '右前']
                cameraNumEl.textContent = imgPosition[ /camera-(\d)/.exec(cameraNumEl.textContent)[1]-1 ]
            }

            [
                [
                    () => ((appear.messageBoxWrap = $('.el-overlay.is-message-box')) && appear.messageBoxWrap?.textContent.startsWith('提示您已长时间未保存数据')),
                    () => {
                        const messageBoxWrap = appear.messageBoxWrap
                        const messageBox = messageBoxWrap.$('.el-message-box')
                        const close = createEl('div', {
                            innerText: '×',
                            style: {
                                position: 'absolute',
                                top: 0,
                                right: 0,
                                width: '50px',
                                height: '50px',
                                fontSize: '30px',
                                textAlign: 'center',
                                zIndex: '99999',
                                cursor: 'pointer',
                            },
                            onclick: function() {
                                messageBoxWrap.remove()
                            }
                        })
                        messageBox.append(close)
                    }
                ],
                [
                    () => {
                        return (an.matches?.('.el-dialog') && an?.textContent.startsWith('物体批注')) ||
                            (an.matches?.('.comment-modal') && an?.parentElement?.previousElementSibling.textContent.startsWith('物体批注'))
                    },
                    () => {
                        const checkboxWrap = an.$('.ant-form-horizontal .ant-row')
                        const checkboxs = an.$$('input[type="checkbox"]')
                        const textarea = an.$('#description')
                        const footer = an.$('form+div')
                        const inputWraps = an.$$('.ant-checkbox-group .ant-col')

                        $('.el-overlay-dialog[aria-label^="物体批注"]').style.opacity = '.95'
                        ;['批注原因', '批注描述'].forEach(tit => (an.$(`label[title="${tit}"]`).parentElement.style.display = 'none'))
                        an.$$('.ant-row').find(item => item.textContent.startsWith('涉及帧数')).style.display = 'none'
                        setStyle(footer, {
                            marginTop: 0,
                            marginBottom: '10px',
                            textAlign: 'center',
                        })
                        inputWraps.forEach(item => (item.style.marginBottom = 0))

                        const quickPhraseWrap = createEl('div', {
                            className: 'quickPhrase-wrap',
                            style: {
                                marginTop: '15px'
                            }
                        })

                        const scheme = {
                            '方位': {
                                '顶视': '顶视图',
                                '侧视': '侧视图',
                                '后视': '后视图',
                                '角度': '角度',
                                '←': '左边框',
                                '↑': '上边框',
                                '→': '右边框',
                                '↓': '下边框',
                                '车头': '车头贴合',
                                '车尾': '车尾贴合',
                                '车顶': '车顶贴合',
                            },
                            '贴合': {
                                '收': '往里收',
                                '扩': '往外扩',
                                '上移': '整体上移',
                                '下移': '整体下移',
                                '左移': '整体左移',
                                '右移': '整体右移',
                                '↶': '逆时针旋转',
                                '↷': '顺时针旋转',
                                '飘空': '飘空',
                                '下陷': '下陷',
                                '稳定边': '贴合稳定边',
                                '地线': '检查地线',
                            },
                            '尺寸': {
                                '长': '长度',
                                '宽': '宽度',
                                '高': '高度',
                                '脑补': '脑补',
                                '统一': '统一尺寸',
                            },
                            '类型': {
                                '小车': '小车',
                                'SUV': 'SUV',
                                '大车': '大车',
                                '两轮车': '两轮车',
                                '三轮车': '三轮车',
                                '人': '人',
                                'BUS': 'BUS',
                                '隔离柱': '隔离柱',
                                '锥桶': '锥桶',
                                '防撞桶': '防撞桶',
                                '防撞球': '防撞球',
                                '一般': '一般障碍物',
                            },
                            '转弯维度': {
                                '未知': '转弯属性:未知',
                                '不转': '转弯属性:不转',
                                '左': '转弯属性:左转',
                                '右': '转弯属性:右转',
                                '双闪': '双闪',
                            },
                            '刹车维度': {
                                '未知': '刹车属性:刹车',
                                '未刹车': '未刹车',
                                '刹车': '刹车',
                            },
                            '其他': {
                                '漏标': '漏标',
                                '前漏': '前续帧漏显示',
                                '后漏': '后续帧漏显示',
                                '漏点': '漏点',
                                '没框全': '没框全',
                                '舍弃点云': '适当舍弃点云',
                            },
                            '补充': {
                                '前后帧检查': '前后帧检查',
                                '伪3D': '参照伪3D',
                            }
                        }
                        const checkboxMap = {
                            '不贴合': ['贴合', '方位'],
                            '标签错误': ['类型'],
                            '方向错误': [],
                            '属性错误': ['转弯维度', '刹车维度'],
                            '尺寸不对': ['尺寸'],
                            '方向错误': [],
                            '多标': [],
                            '其他': ['其他'],
                        }

                        for(let k1 in scheme) {
                            const btnWrap = createEl('div', {
                                className: 'btn-wrap',
                                style: {
                                    display: 'flex',
                                    marginBottom: '12px'
                                }
                            })
                            const title = createEl('div', {
                                className: 'btn-title',
                                innerText: `${k1}:`,
                                style: {
                                    fontSize: '13px'
                                }
                            })
                            btnWrap.append(title)

                            for(let k2 in scheme[k1]) {
                                const phrase = scheme[k1][k2]
                                const btn_style = {
                                    padding: '0px 5px',
                                    height: '20px',
                                    lineHeight: '20px',
                                    margin: '0 5px 0 0',
                                    backgroundColor: 'rgb(136, 136, 136)',
                                    color: 'rgb(255, 255, 255)',
                                    fontSize: '12px',
                                    cursor: 'pointer',
                                    userSelect: 'none',
                                }
                                const btn = createEl('div', {
                                    className: 'phrase-btn',
                                    innerText: k2,
                                    style: btn_style,
                                    onmousedown: function (e) {
                                        const findIdx = Object.values(checkboxMap).findIndex(arr => arr.includes(k1))
                                        if(findIdx !== -1) {
                                            checkboxs.forEach(checkbox => {
                                                if(checkbox.value === Object.keys(checkboxMap)[findIdx] && !checkbox.checked) checkbox.click()
                                            })
                                            if(e.button === 2) {
                                                setTimeout(() => {
                                                    checkboxs.forEach((checkbox, idx) => {
                                                        if(checkbox.value !== Object.keys(checkboxMap)[findIdx] && checkbox.checked) setTimeout(() => checkbox.click(), idx*100)
                                                    })
                                                })
                                            }

                                        }

                                        const value = textarea.value
                                        if(e.button === 0) {
                                            setTextAreaValue(textarea, `${value}${value ? ';' : ''}${phrase}`)
                                        } else if(e.button === 2) {
                                            setTextAreaValue(textarea, `${phrase}`)
                                        }
                                    },
                                })
                                btnWrap.append(btn)
                            }
                            quickPhraseWrap.append(btnWrap)
                        }
                        checkboxWrap.insertAdjacentElement('afterend', quickPhraseWrap)
                        quickPhraseWrap.insertAdjacentElement('afterend', footer)

                    }
                ],
                [
                    () => {
                        return (an.matches?.('.el-dialog') && an?.textContent.startsWith('全局批注')) ||
                            (an.matches?.('.comment-modal') && an?.parentElement?.previousElementSibling.textContent.startsWith('全局批注'))
                    },
                    () => {
                        const checkboxs = an.$$('#reason input[type="checkbox"]')
                        if(checkboxs.every(item => !item.checked)) checkboxs.find(item => item.value == '漏标').click()
                    }
                ],
                [
                    () => !appear.annoType && $('.key-frame-annotate-type .ant-dropdown-trigger'),
                    () => {
                        $('.key-frame-annotate-type .ant-dropdown-trigger').click()
                        appear.annoType = true
                    }
                ],
                [
                    () => !appear.annoTypeDropdown && $('.ant-dropdown')?.textContent.startsWith('关闭关键帧继承'),
                    () => {
                        $('.ant-dropdown').style.display = 'none'

                        $('.key-frame-annotate-type .ant-dropdown-trigger').click()
                        setTimeout(() => ($('.ant-dropdown').style.display = null))
                        appear.annoTypeDropdown = true
                    }
                ],
                [
                    () => !appear.keyFrameInterpolation && $('.ant-dropdown li[data-menu-id="keyFrameInterpolation"]:not(.ant-dropdown-menu-item-disabled)'),
                    () => {
                        $('.ant-dropdown li[data-menu-id="keyFrameInterpolation"]').click()
                        appear.keyFrameInterpolation = true
                    }
                ],
                [
                    () => !appear.mulView && $('.main-view.mul-view'),
                    () => {
                        const mulView = $('.main-view.mul-view')
                        mulView.$$('.el-row.mul-row').forEach(treeView => {
                            bindView(treeView)
                        })

                        $$('.mul-title').forEach(el => {
                            el.onclick = function() {
                                // setInputValue($('.pageIndex input'), )
                                $('.timeline-scale').children[Number(el.textContent) - 1].click()
                            }
                        })

                        appear.mulView = true
                    }
                ],
                [
                    () => !appear.sideviewWrap && $('.sideview-container'),
                    () => {
                        const sideviewWrap = $('.sideview-container')
                        bindView(sideviewWrap)
                        appear.sideviewWrap = true
                    }
                ],
                [
                    () => !appear.sideviewWrap && $('.sideview-container'),
                    () => {

                        appear.sideviewWrap = true
                    }
                ],
                [
                    () => !appear.commentPanel && $('.comment-container'),
                    () => {
                        const commentPanel = $('.comment-container');

                        commentPanel.style.opacity = 0.9

                        let isMoving = false;
                        let top = -15;
                        let right = -225

                        const obsTarget = commentPanel.$('.comment-panel').children[0]
                        Obs(obsTarget, (mrs)=> {
                            if(obsTarget.style.display == '') {
                                commentPanel.style.top = top + 'px'
                                commentPanel.style.right = right + 'px'
                            } else {
                                commentPanel.style.top = '0px'
                                commentPanel.style.right = '3px'
                            }
                        }, {childList: true, attributes: true, attributeOldValue: true})

                        commentPanel.addEventListener('mousedown', (e) => {
                            if(e.button == 1) {
                                isMoving = true
                                e.preventDefault()
                            }
                        })
                        document.body.addEventListener('mousemove', (e) => {
                            if(!isMoving) return
                            commentPanel.style.top = ((top += e.movementY) + 'px')
                            commentPanel.style.right = ((right -= e.movementX) + 'px')
                        })
                        commentPanel.addEventListener('mouseup', (e) => {
                            if(e.button == 1) isMoving = false
                        })
                        appear.commentPanel = true
                    }
                ],
                [
                    () => an.matches?.('.ant-btn.select-label'),
                    () => (_ds.isDrawing = true)
                ],
                [
                    () => !appear.waterMask && $('#waterMask'),
                    () => {
                        $('#waterMask').remove()
                        appear.waterMask = true
                    }
                ],
                [
                    () => !appear.handleLineSide && $('.sideview-container .handle-line-side'),
                    () => {
                        const el = $('.sideview-container .handle-line-side')
                        const obs = Obs(el, mrs => {
                            mrs.forEach(mr => {
                                if(mr.attributeName === 'style' && el.style.diplay !== 'none') {
                                    el.dispatchEvent(new MouseEvent('mousedown', {
                                        screenY: 800
                                    }))
                                    document.body.dispatchEvent(new MouseEvent('mousemove', {
                                        screenY: 620,
                                        bubbles: true
                                    }))
                                    document.body.dispatchEvent(new MouseEvent('mouseup', {
                                        bubbles: true
                                    }))
                                }
                            })
                        }, {childList: true, attributes: true})

                        appear.handleLineSide = true
                    }
                ],
                [
                    () => !appear.objListTitle && $('#pane-objectLabel .header-title'),
                    () => {
                        const objListTitle = $('#pane-objectLabel .header-title')
                        Obs(objListTitle, mrs => {
                            mrs.forEach(mr => {
                                [...mr.addedNodes].forEach(an => {
                                    const curSum = /物体标签\((\d*)\)/.exec(an.textContent)[1]
                                    if(_ds.isDrawing && +curSum === +_ds.objSum+1 ) {
                                        _ds.isDrawing = false
                                        _ds.isFocusObj = true
                                    }
                                    _ds.objSum = curSum

                                })
                            })
                        })
                        appear.objListTitle = true
                    }
                ],
                [
                    () => an.matches?.('.el-tree-node.is-expanded.is-focusable') && an.$('.item-warp-li'),
                    () => {
                        const objItem = an.$('.item-warp-li')
                        if(_ds.isFocusObj && objItem.children[0].matches('.item.active')) {
                            ['keydown', 'keyup'].forEach((event) => {
                                document.body.dispatchEvent(new KeyboardEvent(event, {
                                    code: "Escape",
                                    key: "Escape",
                                    keyCode: 27,
                                    bubbles: true
                                }))
                            });

                            objItem.click()

                            _ds.isFocusObj = false
                        }

                    }
                ],
                [
                    () => an.matches?.('.pc-editor'),
                    () => {
                        setStyle($('.annotate-mode'), {
                            fontSize: '20px',
                            fontWeight: '800',
                            background: 'black',
                        })


                    }
                ],
                [
                    () => !appear.imgView && $('.img-view'),
                    () => {
                        const views = $$('.img-view')
                        views.forEach(view => {
                            let isScroll = false

                            set2DImgPositionInfo(view.$('.camera-number'))

                            clickTrigger(view, (e) => {
                                view.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
                            }, 3, 2)

                            view.addEventListener('mouseenter', (e)=> {
                                if(e.clientX < 20) return
                                view.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
                                isScroll = false
                            })
                            view.addEventListener('mousemove', (e)=> {
                                // console.log(e)
                                if(!isScroll && e.clientX < 20) {
                                    view.dispatchEvent(new MouseEvent('mouseleave'))
                                    isScroll = true
                                } else if(isScroll && e.clientX >= 20) {
                                    view.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
                                    isScroll = false
                                }
                            })
                        })
                        appear.imgView = true
                    },
                ],
                [
                    () => !appear.maxView && (appear.maxView = $('.img-view-max')),
                    () => {
                        const positionInfoEl = [...appear.maxView.$('.info').children].at(-1)
                        set2DImgPositionInfo(positionInfoEl)

                        Obs(positionInfoEl, mrs => {
                            mrs.forEach(mr => {
                                [...mr.addedNodes].forEach(an => {
                                    if(an.nodeName == '#text') set2DImgPositionInfo(an)
                                })
                            })
                        }, {childList: true})

                        clickTrigger(appear.maxView, (e) => {
                            appear.maxView.$('span[title="关闭"]').click()
                        }, 3, 2)
                    }
                ],
                [
                    () => an.$?.('.rect-ground-line line'),
                    () => an.$$('.rect-ground-line line').forEach(line => (line.style.stroke = 'rgba(255, 0, 0, .2'))
                ],
                [
                    () => !appear.attrPanel && $('.main-class-edit'),
                    () => {
                        $('.main-class-edit .view-class-wrap').style.minHeight = 'auto' //高度自适应

                        setStyle($('.main-class-edit'), {
                            bottom: 'auto',
                            top: '260px',
                            left: '1350px',
                            opacity: '0.9',
                        })

                        $('.main-class-edit').addEventListener('mousedown', (e) => {
                            if(e.which == 3) $('.main-class-edit i.el-icon.close').click()
                        })

                        appear.attrPanel = true
                    }
                ],
                [
                    () => !appear.taskWrap && $('.item-wrap.title-task'),
                    () => {
                        const taskWrap = $('.item-wrap.title-task')
                        setStyle(taskWrap, {
                            position: 'relative',
                        })
                        taskWrap.$$('.title-text').forEach(item => {
                            item.style.cursor = 'pointer'

                            item.addEventListener('click', (e) => {
                                let cookieVal
                                document.cookie.split('; ').some(kv => {
                                    const [k, v] = kv.split('=')
                                    if(k === 'ruqimobility.com-prod token') {
                                        cookieVal = v
                                        return true
                                    }
                                })
                                let URLParam = /https:\/\/data-encoder\.ruqimobility\.com\/tool\/pc\?(.*)/.exec(location.href)?.[1]
                                if(!cookieVal || !URLParam) return showMessage('复制失败', {type: 'error'})

                                const textToCopy = cookieVal + '   ' + URLParam
                                copyToClipboard(textToCopy).then(res => showMessage('已复制到剪切板'))
                            })
                        })

                        const btn_change = createEl('div', {
                            title: '切换跳转',
                            style: {
                                position: 'absolute',
                                right: '0px',
                                bottom: '0px',
                                width: '15px',
                                height: '15px',
                                background: 'gray',
                                cursor: 'pointer'
                            },
                            onclick: function() {
                                navigator.clipboard.readText().then((clipText) => {
                                    const [cookieVal, URLParam] = clipText.split('   ')
                                    if(cookieVal && URLParam) {
                                        document.cookie = `ruqimobility.com-prod token=${cookieVal};domain=.ruqimobility.com;path=/`
                                        location.href = `https://data-encoder.ruqimobility.com/tool/pc?${URLParam}`
                                    } else {
                                        showMessage('凭证不合法', {type: 'error'})
                                    }
                                });
                            }
                        })
                        taskWrap.append(btn_change)
                        appear.taskWrap = true
                    },
                ],
                [
                    () => {},
                    () => {},
                ]
            ].forEach((item) => { item[0]() && item[1]() })

            if(an.matches?.('.object-item') && an.textContent?.startsWith('点云对象')) {
                const btnStyle = {
                    position: 'absolute',
                    left: '10px',
                    borderRadius: '5px',
                    transform: 'scale(.9)',
                    padding: '0px 5px',
                    height: '20px',
                    lineHeight: '20px',
                    margin: '0 5px 0 0',
                    backgroundColor: 'rgb(136, 136, 136, .5)',
                    color: 'rgb(255, 255, 255)',
                    fontSize: '12px',
                    cursor: 'pointer',
                    userSelect: 'none',
                }

                const btn_display = createEl('div', {
                    className: 'displayAttr',
                    innerText: '显隐',
                    style: btnStyle,
                    onclick: display.bind(this, void 0)
                })
                const attrPanel = matchesWrapper(an, '.main-class-edit');
                !$('.displayAttr') && attrPanel.$('.edit-class-common').insertAdjacentElement('afterbegin', btn_display)

                display(false)

                function display(isShow) {
                    const items = [...attrPanel.$('.el-collapse').children]
                    const display_items_0 = items[0].style.display
                    items.forEach(el => {
                        if(el.textContent.startsWith('Attributes')) return
                        if(isShow !== void 0) {
                            el.style.display = isShow ? null : 'none'
                        } else {
                            el.style.display = display_items_0 === '' ? 'none' : null
                        }
                    })
                }
                return true
            }
        })
    })

    ++roundCount
}, { childList: true, subtree: true })


function setInputValue(input, value) {
    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
    nativeInputValueSetter.call(input, value);
    ['input', 'change'].forEach((event) => input.dispatchEvent(new Event(event, { bubbles: true })))
};

function setTextAreaValue(textarea, value) {
    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
    nativeInputValueSetter.call(textarea, value);
    ['input', 'change'].forEach((event) => textarea.dispatchEvent(new Event(event, { bubbles: true })))
};

function hijackXHR(change, send) {
    const realXMLHttpRequest = window.XMLHttpRequest;

    window.XMLHttpRequest = function() {
        const xhr = new realXMLHttpRequest();

        xhr.addEventListener('readystatechange', function () {
            if (xhr.readyState !== 4) return
            change.call(this)
        });

        send && (xhr.send = send.bind(xhr))
        return xhr;
    }

    const realOpen = realXMLHttpRequest.prototype.open;
    realXMLHttpRequest.prototype.open = function () {
        const xhr = this
        let curURL = arguments[1]
        const getter = Object.getOwnPropertyDescriptor(realXMLHttpRequest.prototype, "responseText").get
            Object.defineProperty(xhr, "responseText", {
                get() {
                    let result = getter.call(xhr);
                    if(new RegExp("/annotation/dataset/info/\\d+").test(curURL)) {
                        let result_parse = JSON.parse(getter.call(xhr));
                        const classAttributes = result_parse.data.classAttributes
                        if(classAttributes) {
                            classAttributes.pointSize = _ds.pointSize
                            classAttributes.pointColorMode.pointColors = ["rgb(255, 255, 255)", "rgb(255, 132, 0)"];
                        } else {
                            result_parse.data.classAttributes = {
                                "contrast": 100,
                                "saturate": 100,
                                "lightness": 100,
                                "pointSize": 0.17,
                                "measureOpen": true,
                                "measureConfig": [
                                    {
                                        "id": 0,
                                        "type": "circle",
                                        "radius": 50
                                    },
                                    {
                                        "id": 1,
                                        "type": "rect",
                                        "xPlus": 220,
                                        "yPlus": 100,
                                        "rotate": 0,
                                        "xMinus": 220,
                                        "yMinus": 100
                                    },
                                    {
                                        "id": 2,
                                        "type": "rect",
                                        "xPlus": 100,
                                        "yPlus": 50,
                                        "rotate": 0,
                                        "xMinus": 100,
                                        "yMinus": 50
                                    }
                                ],
                                "pointColorMode": {
                                    "mode": "height",
                                    "pointHSL": "hsl(220, 100%, 50%)",
                                    "pointColors": ["rgb(255, 255, 255)", "rgb(255, 132, 0)"],
                                    "pointHeight": [
                                        -0.2,
                                        3.5
                                    ],
                                    "pointIntensity": [
                                        0,
                                        255
                                    ]
                                },
                                "backgroundColor": "#000",
                                "in2DImageRender": {
                                    "renderBox": true,
                                    "renderRect": false,
                                    "renderImgPolygon": true,
                                    "renderProjectBox": true,
                                    "renderImgKeyPoint": true,
                                    "renderImgPolyLine": true
                                }
                            }
                        }
                        // console.log(result)
                        console.log(result_parse)
                        result = JSON.stringify(result_parse)
                    }
                    if(new RegExp("/annotation/projectLabelMatter/findAll/\\d+").test(curURL)) {
                        let result_parse = JSON.parse(getter.call(xhr));
                        const data = result_parse.data
                        const scheme = {
                            '小车': {
                                length: 4.6,
                                height: 1.45,
                            },
                            'SUV': {
                                length: 4.6,
                                width: 2.1,
                                height: 1.65,
                            },
                            'BUS': {
                                length: 10,
                            },
                            '两轮车': {
                                length: 1.6,
                                width: 0.7,
                                height: 1.5,
                            },
                            '三轮车': {
                                length: 2.45,
                                width: 1.10,
                                height: 1.55,
                            },
                            '人': {
                                height: 1.65,
                            },
                            '隔离柱': {
                                length: [0.1, 1, 0.12],
                                width: [0.1, 1, 0.12],
                                height: [0.1, 1, 0.5],
                            },
                            '防撞桶': {
                                length: [0.1, 999, 0.85],
                                width: [0.1, 999, 0.85],
                                height: [0.1, 999, 0.9],
                            },
                            '水马': {
                                length: 1.55,
                                width: 0.55,
                                height: 0.75,
                            },
                            '防撞球': {
                                length: [0.1, 999, 0.4],
                                width: [0.1, 999, 0.4],
                                height: [0.1, 999, 0.45],
                            },
                            '地锁开': {
                                length: [0.1, 1, 0.45],
                                width: [0.1, 1, 0.2],
                                height: [0.1, 1, 0.25],
                            },
                            '隔离栏': {
                                length: 1.65,
                                width: 0.45,
                                height: 1.15,
                            },
                        }
                        if(Array.isArray(data)) {
                            for(let type in scheme) {
                                const target = data.find(item => item.name == type)
                                if(!target) continue

                                for(const whd in scheme[type]) { //长宽高
                                    const size = scheme[type][whd]
                                    if(!Array.isArray(size)) {
                                        target.toolTypeOptions[whd][2] = size
                                    } else {
                                        target.toolTypeOptions[whd] = size
                                        target.toolTypeOptions.isConstraints = true
                                        target.toolTypeOptions.isStandard = true
                                    }
                                }
                            }
                        }
                        console.log(result_parse)
                        result = JSON.stringify(result_parse)
                    }
                    return result
                },
                configurable: true,
            });
        return realOpen.apply(xhr, arguments);
    };
}


function copyToClipboard(textToCopy) {
    // navigator clipboard 需要https等安全上下文
    if (navigator.clipboard && window.isSecureContext) {
        return navigator.clipboard.writeText(textToCopy);
    } else {
        let textArea = document.createElement("textarea");
        textArea.value = textToCopy;
        textArea.style.position = "absolute";
        textArea.style.opacity = 0;
        textArea.style.left = "-999999px";
        textArea.style.top = "-999999px";
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();
        return new Promise((res, rej) => {
            document.execCommand('copy') ? res() : rej();
            textArea.remove();
        });
    }
}

function matchesWrapper(el, sel) {
    const parentEl = el.parentElement
    if(!parentEl) return null

    if(parentEl.matches(sel)) {
        return parentEl
    } else {
        return matchesWrapper(parentEl, sel)
    }
}

function clickTrigger(el, fn, button, moveThreshold = 0) {
    let movement = 0
    let allowTrigger = false
    let isRightdown = false

    el.addEventListener('mousedown', (e)=>{
        e.preventDefault()
        if(e.which !== button) return
        movement = 0
        isRightdown = true
        allowTrigger = true
    })
    el.addEventListener('mousemove', (e)=>{
        if(!isRightdown || (isRightdown && (movement+=(Math.sqrt(e.movementX**2 + e.movementY**2))) <= moveThreshold)) return

        allowTrigger && (allowTrigger = false)
    })
    el.addEventListener('mouseup', (e)=>{
        if(e.which !== button) return
        if(allowTrigger) fn(e)
        isRightdown = false
    })
}

function Obs(target, callBack, options = { childList: true, subtree: true, attributes: true, attributeOldValue: true}) {
    if(!target) return console.error('目标不存在')

    const ob = new MutationObserver(callBack);
    ob.observe(target, options);
    return ob
}


function setStyle() {
    [[Map, ()=> {
        const styleMap = arguments[0]
        for (const [el, styleObj] of styleMap) {
            !Array.isArray(el) ? setStyleObj(el, styleObj) : el.forEach((el) => setStyleObj(el, styleObj))
        }
    }], [Element, () => {
        const [el, styleObj] = arguments
        setStyleObj(el, styleObj)
    }], [Array, () => {
        const [els, styleObj] = arguments
        els.forEach((el) => setStyleObj(el, styleObj))
    }]].some(([O, fn]) => O.prototype.isPrototypeOf(arguments[0]) ? (fn(), true) : false)

    function setStyleObj(el, styleObj) {
        for (const attr in styleObj) {
            if (el.style[attr] !== undefined) {
                el.style[attr] = styleObj[attr]
            } else {
                //将key转为标准css属性名
                const formatAttr = attr.replace(/[A-Z]/, match => `-${match.toLowerCase()}`)
                console.error(el, `的 ${formatAttr} CSS属性设置失败!`)
            }
        }
    }
}

function createEl(elName, options) {
    const el = document.createElement(elName)
    for(let opt in options) {
        if(opt !== 'style') {
            el[opt] = options[opt]
        } else {
            let styles = options[opt]
            setStyle(el, styles)
        }
    }
    return el
}


function $(selector) {
    const _this = Element.prototype.isPrototypeOf(this) ? this : document
    const sel = String(selector).trim();

    const id = /^#([^ +>~\[:]*)$/.exec(sel)?.[1]
    return (id && _this === document) ? _this.getElementById(id) : _this.querySelector(sel)
}

function $$(selector) {
    const _this = Element.prototype.isPrototypeOf(this) ? this : document
    return Array.from(_this.querySelectorAll(selector))
}

function getParamValue(param) {
    let r
    location.href.split('?')[1].split('&').some(item => {
        const param_value = item.split('=')
        if(param_value[0] == param) {
            r = param_value[1]
            return true
        }
    })
    return r
}

function showMessage(message, config) { //type = 'default', showTime = 3000, direction
    let oldMessageWrap = document.querySelector(`.messageWrap-${config?.direction ? config.direction : 'top'}`)

    let MessageWrap
    if(!oldMessageWrap) {
        MessageWrap = document.createElement('div')
        MessageWrap.className = 'messageWrap'
        setStyle(MessageWrap, {
            position: 'absolute',
            zIndex: '9999'
        })
    } else {
        MessageWrap = oldMessageWrap
    }

    let MessageBox = document.createElement('div')
    MessageBox.innerText = message

    let closeBtn = document.createElement('div')
    closeBtn.textContent = '×'
    closeBtn.addEventListener('click', MessageBox.remove.bind(MessageBox)) //关闭消息提示

    setStyle(MessageBox, {
        position: 'relative',
        minWidth: '200px',
        marginTop: '5px',
        padding: '6px 50px',
        lineHeight: '25px',
        backgroundColor: 'pink',
        textAlign: 'center',
        fontSize: '16px',
        borderRadius: '5px',
        transition: 'all 1s'
    })

    setStyle(closeBtn, {
        position: 'absolute',
        top: '-3px',
        right: '3px',
        width: '15px',
        height: '15px',
        zIndex: '999',
        fontWeight: '800',
        fontSize: '15px',
        borderRadius: '5px',
        cursor: 'pointer',
        userSelect: 'none'
    })
    //控制方向
    switch(config?.direction) {
        case 'top': setStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
        case 'top left': setStyle(MessageWrap, {top: '1%', left: '.5%'}); break;
        case 'left': setStyle(MessageWrap, {top: '50%', left: '1%', transform: 'translateY(-50%)'}); break;
        case 'top right': setStyle(MessageWrap, {top: '1%', right: '.5%', }); break;
        case 'right': setStyle(MessageWrap, {top: '50%', right: '.5%', transform: 'translateY(-50%)'}); break;
        case 'center': setStyle(MessageWrap, {top: '20%', left: '50%', transform: 'translate(-50%, -50%)'}); break;
        case 'bottom': setStyle(MessageWrap, {bottom: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
        case 'bottom8': setStyle(MessageWrap, {bottom: '8%', left: '50%', transform: 'translate(-50%, -50%)'}); break;
        case 'bottom left': setStyle(MessageWrap, {bottom: '1%'}); break;
        case 'bottom right': setStyle(MessageWrap, {bottom: '1%', right: '.5%'}); break;
        default: setStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
    }

    switch(config?.type) {
        case 'success': setStyle(MessageBox, {border: '1.5px solid rgb(225, 243, 216)', backgroundColor: 'rgb(240, 249, 235)', color: 'rgb(103, 194, 58)'}); break;
        case 'warning': setStyle(MessageBox, {border: '1.5px solid rgb(250, 236, 216)', backgroundColor: 'rgb(253, 246, 236)', color: 'rgb(230, 162, 60)'}); break;
        case 'error': setStyle(MessageBox, {border: '1.5px solid rgb(253, 226, 226)', backgroundColor: 'rgb(254, 240, 240)', color: 'rgb(245, 108, 108)'}); break;
        default: setStyle(MessageBox, {border: '1.5px solid rgba(202, 228, 255) ', backgroundColor: 'rgba(236, 245, 255)', color: 'rgb(64, 158, 255)'}); break;
    }

    MessageBox.appendChild(closeBtn)
    if(oldMessageWrap) {
        oldMessageWrap.appendChild(MessageBox)
    } else {
        MessageWrap.appendChild(MessageBox)
        document.body.appendChild(MessageWrap)
    }
    let ani = MessageBox.animate([{
        transform: "translate(0, -100%)" ,
        opacity: 0.3,
    },{
        transform: "translate(0, 18px)",
        opacity: 0.7,
        offset: 0.9,
    },{
        transform: "translate(0, 15px)",
        opacity: 1,
        offset: 1,
    }], {
        duration: 300,
        fill: 'forwards',
        easing: 'ease-out',
    })

    //控制消失
    let timer = setTimeout(() => {
        ani.onfinish = () => {
            MessageBox.remove()
        }
        ani.reverse()
    }, (config?.showTime || 3000))

    //鼠标悬停时不清除,离开时重新计时
    MessageBox.addEventListener('mouseenter', () => clearTimeout(timer))
    MessageBox.addEventListener('mouseleave', () => {
        timer = setTimeout(() => {
            ani.reverse()
            ani.onfinish = () => {
                MessageBox.remove()
            }
        }, (config?.showTime || 3000))
    })
}


/**
2024/12/25:
- 新增:简化属性面板

2024/12/26:
- 新增:【Tab】前进一帧,【Tab+Shift】回退一帧
- 新增:右键缩放映射图,鼠标进入映射图后滚动调整缩放比,贴边滚动切换映射图。
- 调整:总框数的显示效果
- 优化:淡化吸附地线高度提示颜色
- 优化:隐藏右键菜单
- 新增:鼠标右键关闭属性面板

2024/12/27
- 新增:切包跳转

2025/1/5
- 优化:建框完成后退出绘制状态
- 优化:【Y】键聚焦视距

2025/1/6
- 调整:数据包跳转编码

2025/1/12
- 新增:title 展示提包ID

2025/1/15
- 新增:调整三视图高度

2025/2/9
- 调整:批注列表位置
- 调整:去除账号水印
- 新增:鼠标中键拖动批注列表
- 新增:作业记录title显示任务id

2025/2/10
- 新增:重写三视图调整快捷键
- 修复:无法进入未配置 classAttributes 的数据包

2025/2/11
- 【`】切换单帧/多帧视图
- 多帧视图下翻页
- 【空格】下一帧
- 【Tab】上一帧
- 【1】小车
- 【2】SUV
- 【3】二轮
- 【4】行人
- 【5】隔离柱
- 多帧视图点击序号切帧

2025/2/12
- 新增:调整现有默认尺寸,对无默认尺寸的类型进行补充
- 修复:空格的副作用(触发任意点击过的按钮)
- 新增:关键帧标注方案默认勾选[关键帧插值]
- 新增:全局批注默认勾选[漏标]

2025/2/13
- 新增:调整映射图视角名称
*/