// ==UserScript== // @name Utilities // @namespace KrzysztofKruk-FlyWire // @version 0.22.1 // @description Various functionalities for FlyWire // @author Krzysztof Kruk // @match https://ngl.flywire.ai/* // @match https://edit.flywire.ai/* // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect services.itanna.io // @connect prodv1.flywire-daf.com // @updateURL https://raw.githubusercontent.com/ChrisRaven/FlyWire-Utilities/main/Utilities.user.js // @downloadURL https://raw.githubusercontent.com/ChrisRaven/FlyWire-Utilities/main/Utilities.user.js // @homepageURL https://github.com/ChrisRaven/FlyWire-Utilities // ==/UserScript== // addon prefix - used in all nodes, that have to have an ID const ap = 'kk-utilities-' const op = ap + 'option-' const TYPES = { CHECKBOX: 1, TEXT: 2, NUMBER: 3, TEXTAREA: 4, RANGE: 5 } const options = { jumpToStart: { type: TYPES.CHECKBOX, optionSelector: op + 'jump-to-start', featureSelector: `#${ap}jump-to-start`, text: 'Jump to start' }, addAtStart: { type: TYPES.CHECKBOX, optionSelector: op + 'add-point-at-start', featureSelector: `#${ap}add-annotation-at-start-wrapper`, text: 'Add point at start' }, removePointsAtStart: { type: TYPES.CHECKBOX, optionSelector: op + 'remove-points-at-start', featureSelector: `#${ap}remove-annotations-at-start-wrapper`, text: 'Remove points at start' }, resolutionButtons: { type: TYPES.CHECKBOX, optionSelector: op + 'resolution-buttons', featureSelector: `#${ap}res-wrapper`, text: 'Resolution buttons' }, toggleBackground: { type: TYPES.CHECKBOX, optionSelector: op + 'toggle-background', featureSelector: `#${ap}toggle-background`, text: 'Background color switch' }, displayNumberOfSegments: { type: TYPES.CHECKBOX, optionSelector: op + 'display-number-of-segments', featureSelector: `#${ap}display-number-of-segments`, text: 'Display number of segments' }, showNeuropils: { type: TYPES.CHECKBOX, optionSelector: op + 'show-neuropils', featureSelector: `#${ap}show-neuropils`, text: 'Show neuropils' }, copyPosition: { type: TYPES.CHECKBOX, optionSelector: op + 'copy-position', featureSelector: `#${ap}copy-position-wrapper`, text: 'Copy position' }, removeWithCtrlShift: { type: TYPES.CHECKBOX, optionSelector: op + 'remove-with-ctrl-shift', text: 'Remove segments when Ctrl and Shift are pressed' }, hideWithAltShift: { type: TYPES.CHECKBOX, optionSelector: op + 'hide-with-alt-shift', text: 'Hide segments when Alt and Shift are pressed' }, neuropils: { isGroup: true, neuropils_opticLobe: { type: TYPES.CHECKBOX, optionSelector: op + 'neuropil-optic-lobe', text: 'Show Optic Lobe' }, neuropils_medulla: { type: TYPES.CHECKBOX, optionSelector: op + 'neuropil-medulla', text: 'Show Medulla' }, neuropils_lobula: { type: TYPES.CHECKBOX, optionSelector: op + 'neuropil-lobula', text: 'Show Lobula' }, neuropils_lobulaPlate: { type: TYPES.CHECKBOX, optionSelector: op + 'neuropil-lobula-plate', text: 'Show Lobula Plate' }, neuropils_accessoryMedulla: { type: TYPES.CHECKBOX, optionSelector: op + 'neuropil-accessory-medulla', text: 'Show Accessory Medulla' }, neuropils_blackBackgroundTransparency: { type: TYPES.TEXT, optionSelector: op + 'neuropil-transparency-on-black', text: 'Neuropil transparency on black background' }, neuropils_whiteBackgroundTransparency: { type: TYPES.TEXT, optionSelector: op + 'neuropil-transparency-on-white', text: 'Neuropil transparency on white background' } } } let shift = false let ctrl = false let alt = false let removeWithCtrlShift = false let hideWithAltShift = false let saveable = { startCoords: null, addAnnotationAtStartState: false, removeAnnotationsAtStartState: false, startAnnotationId: 0, visibleFeatures: { jumpToStart: true, addAtStart: true, removePointsAtStart: true, resolutionButtons: true, background: true, neuropils: true, copyPosition: true, removeWithCtrlShift: false, hideWithAltShift: false }, neuropils_opticLobe: true, neuropils_medulla: true, neuropils_lobula: true, neuropils_lobulaPlate: true, neuropils_accessoryMedulla: true, neuropils_blackBackgroundTransparency: 0.1, neuropils_whiteBackgroundTransparency: 0.05, currentResolutionButton: 1, backgroundColor: 'black', displayNumberOfSegments: true } if (!document.getElementById('dock-script')) { let script = document.createElement('script') script.id = 'dock-script' script.src = typeof DEV !== 'undefined' ? 'http://127.0.0.1:5501/FlyWire-Dock/Dock.js' : 'https://chrisraven.github.io/FlyWire-Dock/Dock.js' document.head.appendChild(script) } function fix_segmentColors_2022_07_15() { if (Dock.ls.get('fix_segmentColors_2022_07_15') === 'fixed') return Object.entries(localStorage).forEach(entry => { if (entry[0].includes('neuroglancerSaveState_v2-')) { let e = JSON.parse(entry[1]) if (e.state && e.state.layers) { e.state.layers.forEach(layer => { if (layer.type === 'segmentation_with_graph' && layer.segmentColors) { layer.segmentColors = {} localStorage.setItem(entry[0], JSON.stringify(e)) } }) } } }) Dock.ls.set('fix_segmentColors_2022_07_15', 'fixed') } function fix_visibilityOptions_2022_07_30() { if (Dock.ls.get('fix_visibilityOptions_2022_07_30') === 'fixed') return let settings = Dock.ls.get('utilities', true) if (!settings) return if (!settings.options) return settings.options['kk-utilities-options-toggle-resolution-buttons'].selector = '#kk-utilities-res-wrapper' Dock.ls.set('utilities', settings, true) Dock.ls.set('fix_visibilityOptions_2022_07_30', 'fixed') } function fix_optionsOrganization_2022_08_15() { if (Dock.ls.get('fix_optionsOrganization_2022_08_15') === 'fixed') return let settings = Dock.ls.get('utilities', true) if (!settings) return function update(oldName, newName) { let option let value option = settings.options[oldName] if (!option) return value = option.value === undefined ? option.state : option.value settings[newName] = value } if (settings.options) { update('kk-utilities-options-toggle-jump-to-start', 'jumpToStart') update('kk-utilities-options-toggle-add-point-at-start', 'addAtStart') update('kk-utilities-options-toggle-remove-points-at-start', 'removePointsAtStart') update('kk-utilities-options-toggle-resolution-buttons', 'resolutionButtons') update('kk-utilities-options-toggle-toggle-background', 'toggleBackground') update('kk-utilities-options-toggle-show-neuropils', 'showNeuropils') delete settings.options } let leaves = {} for(const [key, value] of Object.entries(settings.leaves)) { if (value[0] !== null && value[0] !== undefined && !isNaN(value[0])) { leaves[key] = value } } settings.leaves = leaves let roots = {} for(const [key, value] of Object.entries(settings.roots)) { if (value[0] !== null && value[0] !== undefined && !isNaN(value[0])) { roots[key] = value } } settings.roots = roots Dock.ls.set('utilities', settings, true) Dock.ls.set('fix_optionsOrganization_2022_08_15', 'fixed') } function fix_removeLeavesAndRoots_2023_05_31() { if (Dock.ls.get('fix_removeLeavesAndRoots_2023_05_31') === 'fixed') return let settings = Dock.ls.get('utilities', true) if (!settings) return delete settings.leaves delete settings.roots Dock.ls.set('utilities', settings, true) Dock.ls.set('fix_removeLeavesAndRoots_2023_05_31', 'fixed') } function loadFromLS() { let data = Dock.ls.get('utilities', true) if (data) { Dock.mergeObjects(saveable, data) removeWithCtrlShift = saveable.visibleFeatures.removeWithCtrlShift hideWithAltShift = saveable.visibleFeatures.hideWithAltShift } } function saveToLS() { Dock.ls.set('utilities', saveable, true) } document.addEventListener('dock-ready', () => { unsafeWindow.GM_xmlhttpRequest = GM_xmlhttpRequest main() }) function main() { // this fix at the beginning, because we have to fix the options, before accessing them in the rest of the main() function fix_optionsOrganization_2022_08_15() loadFromLS() let optionsDialog = Dock.dialog(optionsDialogSettings()) let dock = new Dock() dock.addAddon({ name: 'Utilities', id: ap, html: generateHtml(), css: /*css*/` #${ap} { text-align: center; } #${ap}jump-to-start, #${ap}add-annotation-at-start-wrapper, #${ap}remove-annotations-at-start-wrapper, #${ap}show-neuropils, #${ap}res-wrapper, #${ap}copy-position { display: block; } #${ap}toggle-background, #${ap}jump-to-start, #${ap}show-neuropils { margin: auto; } .selected-segment-button { border: 2px solid orange; } #${ap}display-number-of-segments { display: inline-block; margin-top: 9px; padding-left: 10px; } `, events: { '.neuroglancer-layer-side-panel': { contextmenu: (e) => { jumpToSegment(e) openSegmentsInNewTabHandler(e) } }, [`#${ap}jump-to-start`]: { click: jumpToStart }, [`.${ap}res`]: { click: e => changeResolution(e) }, [`#${ap}add-annotation-at-start`]: { click: addAnnotationAtStartChanged }, [`#${ap}remove-annotations-at-start`]: { click: removeAnnotationsAtStartChanged }, [`#${ap}options`]: { click: () => optionsDialog.show() }, [`#${ap}options-dialog`]: { click: (e) => optionsDialogToggleFeatures(e), input: (e) => optionsDialogTextInputHandler(e) }, [`#${ap}toggle-background`]: { click: toggleBackground }, [`#${ap}show-neuropils`]: { click: showNeuropils }, [`#${ap}copy-position-copy`]: { click: copyPosition }, [`#${ap}copy-position-paste`]: { click: pastePosition } } }) document.addEventListener('fetch', e => fetchHandler(e)) document.addEventListener('contextmenu', e => hideAllButHandler(e)) initFields() Dock.addToMainTab('segmentation_with_graph', assignMainTabEvents) Dock.addToRightTab('segmentation_with_graph', 'Rendering', displayNumberOfSegments) fix_segmentColors_2022_07_15() fix_visibilityOptions_2022_07_30() fix_removeLeavesAndRoots_2023_05_31() document.addEventListener('keydown', e => { if (e.ctrlKey) { ctrl = true } if (e.shiftKey) { shift = true } if (e.altKey) { alt = true } }) document.addEventListener('keyup', e => { // e.ctrlKey and e.shiftKey don't work for some reason if (e.key === 'Control') { ctrl = false } if (e.key === 'Shift') { shift = false } if (e.key === 'Alt') { alt = false } }) let prevPrevId = null let prevId = null viewer.mouseState.changed.add(() => { if (ctrl && shift && removeWithCtrlShift) { const id = viewer.mouseState.pickedValue.toJSON() if (id && prevId && prevPrevId && prevId === id && prevPrevId === id) { const element = document.querySelector(`button[data-seg-id="${id}"]`) if (element) { element.click() } } prevPrevId = prevId prevId = id } if (alt && shift && hideWithAltShift) { const id = viewer.mouseState.pickedValue.toJSON() if (id && prevId && prevPrevId && prevId === id && prevPrevId === id) { let element = document.querySelector(`button[data-seg-id="${id}"]`) if (element) { element = element.parentElement.querySelector('input[type="checkbox"]') element.click() } } prevPrevId = prevId prevId = id } }) } function assignMainTabEvents() { // setTimeout, because the changed event is called, when the elements aren't yet available in the DOM setTimeout(() => { document.getElementsByClassName('neuroglancer-rendered-data-panel') .forEach(panel => panel.addEventListener('contextmenu', (e) => { deleteAnnotationPoint(e) deleteSplitPoint(e) jumpToSegmentButton(e) })) }, 0) } function fetchHandler(e) { let response = e.detail.response let body = e.detail && e.detail.params ? e.detail.params.body : null let url = e.detail.url if (response.code && response.code === 400) return console.error('Utilities: failed operation') if (url.includes('proofreading_drive?')) { saveSegmentAfterClaim(response) deletePointsAtStart() addAnnotationAtStart() saveToLS() } else if (url.includes('split_preview?')) { if (!response.illegal_split) return let separatedSupervoxels = response.supervoxel_connected_components[2] if (!separatedSupervoxels || !separatedSupervoxels.length) return body = JSON.parse(body) highlightSeparatedSupervoxels(body, separatedSupervoxels) } } function highlightSeparatedSupervoxels(body, separatedSupervoxels) { separatedSupervoxels.forEach(separatedSupervoxel => { body.sinks.forEach(sink => { if (sink[0] !== separatedSupervoxel) return document.querySelector(`[data-seg-id="${separatedSupervoxel}"]`).style.border = '2px solid orange' }) body.sources.forEach(source => { if (source[0] !== separatedSupervoxel) return document.querySelector(`[data-seg-id="${separatedSupervoxel}"]`).style.border = '2px solid orange' }) }) } function jumpToSegment(e) { if (!e.target.classList.contains('segment-button')) return if (e.ctrlKey) return let segId = Object.keys(e.target.dataset).length && e.target.dataset.segId viewer.selectedLayer.layer_.layer_.meshLayer.chunkManager.memoize.map.forEach(el => { if (el.constructor.name !== 'GrapheneMeshSource') return for (const [key, value] of el.chunks) { const keyAsInts = key.split(',').map(num => parseInt(num, 10)) const keyAsString = new Uint64(...keyAsInts).toJSON() if (keyAsString !== segId) continue const firstFragmentId = value.fragmentIds[0].split(':')[0] for (const [fragmentId, data] of value.source.fragmentSource.chunks) { const id = fragmentId.split(':')[0] if (id !== firstFragmentId) continue const positions = data.meshData.vertexPositions const point = Array.prototype.slice.call(positions, 0, 3) const targetPosition = Dock.divideVec3(point, Dock.getVoxelSize()) Dock.jumpToCoords(targetPosition) break } break } }) } function jumpToSegmentButton(e) { if (!e.ctrlKey) return document.getElementsByClassName('selected-segment-button').forEach(el => el.classList.remove('selected-segment-button')) // "selectedSeg" class is added automatically, whenever user hovers their mouse cursor over a segment in 2D or 3D const element = document.getElementsByClassName('selectedSeg')[0] if (!element) return element.scrollIntoView() element.parentElement.classList.add('selected-segment-button') } function openSegmentsInNewTabHandler(e) { let button = e.target // e.target ===
` }