// ==UserScript== // @name 一键切换 RGB 着色 // @namespace swj.pointcloud.rgb-shortcut // @version 1.0.0 // @description 在点云标注平台按 M 在 RGB 着色与切换前着色模式之间往返。 // @author SWJ // @match *://*/map-pointcloud/* // @run-at document-idle // @grant unsafeWindow // @noframes // ==/UserScript== (function () { 'use strict'; const APP_ID = 'swj-pointcloud-rgb-shortcut'; const STORAGE_KEY = `${APP_ID}:last-non-rgb-mode`; const RGB_COLOR_TYPE = 3; const DEFAULT_COLOR_TYPE = 1; const TOGGLE_KEY = 'm'; let lastNonRgbMode = readStoredMode(); let toastTimer = 0; let helpInjectQueued = false; function pageWindow() { return typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; } function runtime() { const page = pageWindow(); const instance = page.instance; return { instance, store: instance && instance.StoreManager, viewer: page.viewer || (instance && instance._viewer), }; } function validNonRgbMode(value) { return value === 1 || value === 2 || value === 4; } function readStoredMode() { try { const value = Number(localStorage.getItem(STORAGE_KEY)); return validNonRgbMode(value) ? value : DEFAULT_COLOR_TYPE; } catch (_) { return DEFAULT_COLOR_TYPE; } } function storeMode(value) { if (!validNonRgbMode(value)) return; lastNonRgbMode = value; try { localStorage.setItem(STORAGE_KEY, String(value)); } catch (_) { // localStorage may be disabled; the in-memory fallback still works. } } function isInteractiveTarget(event) { const path = typeof event.composedPath === 'function' ? event.composedPath() : []; const source = path[0] || event.target; const element = source && source.nodeType === 1 ? source : source && source.parentElement; return !!( element && typeof element.closest === 'function' && element.closest( 'input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"]', ) ); } function hasRgbAttribute(viewer) { if (viewer && viewer.supportRgbColor === true) return true; const geometry = viewer && viewer.points && viewer.points.geometry; return !!(geometry && geometry.attributes && geometry.attributes.color); } function supportsPreviousMode(viewer, mode) { if (mode === 3) return hasRgbAttribute(viewer); if (mode === 2) return !viewer || viewer.supportIntensityColor !== false; return mode === 1 || mode === 4; } function showToast(message, kind) { let host = document.getElementById(`${APP_ID}-toast`); if (!host) { host = document.createElement('div'); host.id = `${APP_ID}-toast`; host.style.cssText = [ 'position:fixed', 'top:76px', 'left:50%', 'transform:translateX(-50%)', 'z-index:2147483647', 'pointer-events:none', ].join(';'); const shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = `
`; document.documentElement.appendChild(host); } const toast = host.shadowRoot.querySelector('.toast'); toast.className = `toast ${kind || ''}`; toast.textContent = message; requestAnimationFrame(() => toast.classList.add('show')); clearTimeout(toastTimer); toastTimer = window.setTimeout(() => toast.classList.remove('show'), 1600); } function switchRgb() { const { store, viewer } = runtime(); if (!store || !viewer) { showToast('点云视图尚未初始化,请稍后再试。', 'error'); return; } let state; try { state = store.statusState(); } catch (_) { showToast('无法读取点云状态,请刷新页面后重试。', 'error'); return; } if (!state || !state.pcdLoaded || !viewer.points) { showToast('请等待点云加载完成后再切换。', 'error'); return; } const currentMode = Number(state.colorType); if (currentMode !== RGB_COLOR_TYPE) storeMode(currentMode); if (!hasRgbAttribute(viewer)) { showToast('当前点云没有 RGB 属性,无法切换 RGB 着色。', 'error'); return; } let targetMode = RGB_COLOR_TYPE; if (currentMode === RGB_COLOR_TYPE) { targetMode = validNonRgbMode(lastNonRgbMode) ? lastNonRgbMode : DEFAULT_COLOR_TYPE; if (!supportsPreviousMode(viewer, targetMode)) targetMode = DEFAULT_COLOR_TYPE; } try { const colorValue = state.colorValue; store.updateStatusProperty('colorType', targetMode); viewer.setPCDColorModel(targetMode, colorValue); showToast(targetMode === RGB_COLOR_TYPE ? '已切换:RGB 着色' : '已切回:非 RGB 着色', 'success'); } catch (error) { console.error('[RGB快捷键] 切换失败', error); showToast(`RGB 着色切换失败:${error && error.message ? error.message : '未知错误'}`, 'error'); } } function injectShortcutHelp() { const root = document.getElementById('top-shortkey'); if (!root) return; const list = root.querySelector('#shortcuts-scroll .el-scrollbar__view') || root.querySelector('#shortcuts-scroll') || root; if (list.querySelector('[data-rgb-shortcut="m"]')) return; const item = document.createElement('div'); item.className = 'top-shortkey-item'; item.dataset.rgbShortcut = 'm'; const key = document.createElement('p'); key.className = 'top-shortkey-left'; key.textContent = 'M'; const description = document.createElement('p'); description.className = 'top-shortkey-right'; description.textContent = '切换 RGB 着色'; item.append(key, description); list.prepend(item); } function scheduleShortcutHelpInjection() { if (helpInjectQueued) return; helpInjectQueued = true; requestAnimationFrame(() => { helpInjectQueued = false; injectShortcutHelp(); }); } function onKeyDown(event) { if (event.defaultPrevented || event.repeat || event.isComposing) return; if (event.key && event.key.toLowerCase() !== TOGGLE_KEY) return; if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return; if (isInteractiveTarget(event)) return; const { store } = runtime(); try { if (store && typeof store.isDisabledShortKey === 'function' && store.isDisabledShortKey()) { return; } } catch (_) { return; } event.preventDefault(); event.stopPropagation(); switchRgb(); } document.addEventListener('keydown', onKeyDown, true); const observer = new MutationObserver(() => { const shortcutRoot = document.getElementById('top-shortkey'); if (shortcutRoot && !shortcutRoot.querySelector('[data-rgb-shortcut="m"]')) { scheduleShortcutHelpInjection(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); scheduleShortcutHelpInjection(); })();