import { ff } from "@humansignal/core"; import { destroy } from "mobx-state-tree"; import { FF_DEV_3391 } from "../utils/feature-flags"; import { guidGenerator } from "../utils/unique"; /** @type {Map} */ const INSTANCES = new Map(); let root = null; class ToolsManager { static getInstance({ name } = {}) { if (!name) return; if (INSTANCES.has(name)) { return INSTANCES.get(name); } const instance = new ToolsManager({ name }); INSTANCES.set(name, instance); return instance; } static allInstances() { return Array.from(INSTANCES.values()); } static setRoot(rootStore) { root = rootStore; } static removeAllTools() { INSTANCES.forEach((manager) => manager.removeAllTools()); INSTANCES.clear(); } constructor({ name } = {}) { this.name = name; this.tools = {}; this._default_tool = null; this._prefix = guidGenerator(); } get preservedTool() { return window.localStorage.getItem(`selected-tool:${this.name}`); } /** There are some problems with working with ToolManager with interactive view all flag switched on. For now, tool manager is hidden in view_all, so it allows us to use root and selected annotation while we are looking for the object or the control from the tool. At the same time, we can use `annotation_id` as an additional key to be able to get the right annotation in that view_all mode. But in that case, there will be a problem with the inconsistent state of tool manager for 2 different annotations in the context of the same task. */ get root() { return root; } get obj() { if (ff.isActive(FF_DEV_3391)) { return root.annotationStore.selected?.names.get(this.name); } return root.annotationStore.names.get(this.name); } addTool(toolName, tool, removeDuplicatesNamed = null, prefix = guidGenerator()) { if (tool.smart && tool.control?.smartonly) return; // todo: It seems that key is used only for storing, // but not for finding tools, so may be there might // be an array instead of an object const name = tool.toolName ?? toolName; const key = `${prefix ?? this._prefix}#${name}`; if (removeDuplicatesNamed && toolName === removeDuplicatesNamed) { const findme = new RegExp(`^.*?#${name}.*$`); if (Object.keys(this.tools).some((entry) => findme.test(entry))) { console.log( `Ignoring duplicate tool ${name} because it matches removeDuplicatesNamed ${removeDuplicatesNamed}`, ); return; } } this.tools[key] = tool; if (tool.default && !this._default_tool) this._default_tool = tool; if (this.preservedTool && tool.shouldPreserveSelectedState) { if (tool.fullName === this.preservedTool && tool.setSelected) { this.unselectAll(); this.selectTool(tool, true, true); return; } } if (this._default_tool && !this.hasSelected) { this.selectTool(this._default_tool, true, true); } } unselectAll() { // when one of the tool get selected you need to unselect all // other active tools Object.values(this.tools).forEach((t) => { if (typeof t.selected !== "undefined") t.setSelected(false); }); const stage = this.obj?.stageRef; if (stage) { stage.container().style.cursor = "default"; } } selectTool(tool, selected, isInitial = false) { const currentTool = this.findSelectedTool(); const newSelection = tool?.group; // if there are no tools selected, there are no specific labels to unselect // also this will skip annotation init if (currentTool && newSelection === "segmentation") { const toolType = tool.control.type.replace(/labels$/, ""); const currentLabels = tool.obj.activeStates(); // labels of different types; we can't create regions with different tools simultaneously, so we have to unselect them const unrelatedLabels = currentLabels.filter((tag) => { const type = tag.type.replace(/labels$/, ""); if (tag.type === "labels") return false; if (type === toolType) return false; return true; }); unrelatedLabels.forEach((tag) => tag.unselectAll()); } currentTool?.handleToolSwitch?.(tool); currentTool?.complete?.(); if (selected) { this.unselectAll(); tool.setSelected?.(true, isInitial); } else { const drawingTool = this.findDrawingTool(); this.selectTool(drawingTool ?? this._default_tool, true); } } selectDefault() { const tool = this.findSelectedTool(); if (this._default_tool && tool?.dynamic === true) { this.unselectAll(); this._default_tool.setSelected(true); } } allTools() { return Object.values(this.tools); } addToolsFromControl(s) { if (s.tools) { const t = s.tools; Object.keys(t).forEach((k) => { this.addTool(k, t[k], s.removeDuplicatesNamed, s.name || s.id); }); } } findSelectedTool() { return Object.values(this.tools).find((t) => t.selected); } findDrawingTool() { return Object.values(this.tools).find((t) => t.isDrawing); } event(name, ev, ...args) { // if there is an active tool, dispatch there const selectedTool = this.findSelectedTool(); if (selectedTool) { selectedTool.event(name, ev, args); return; } } reload({ name } = {}) { INSTANCES.delete(this.name); INSTANCES.set(name, this); this.removeAllTools(); this.name = name; } removeAllTools() { Object.values(this.tools).forEach((t) => destroy(t)); this.tools = {}; this._default_tool = null; } get hasSelected() { return Object.values(this.tools).some((t) => t.selected); } } window.ToolManager = ToolsManager; export default ToolsManager;