/**
* OpenSeadragon paperjs overlay plugin based on paper.js
* @version 0.7.6
*
* Copyright (c) 2022-2026, Thomas Pearce
* All rights reserved.
*/
import { ToolBase } from './papertools/base.mjs';
import { DefaultTool } from './papertools/default.mjs';
import { WandTool } from './papertools/wand.mjs';
import { BrushTool } from './papertools/brush.mjs';
import { PointTool } from './papertools/point.mjs';
import { PointTextTool } from './papertools/pointtext.mjs';
import { RectangleTool } from './papertools/rectangle.mjs';
import { EllipseTool } from './papertools/ellipse.mjs';
import { StyleTool } from './papertools/style.mjs';
import { LinestringTool } from './papertools/linestring.mjs';
import { RulerTool } from './papertools/ruler.mjs';
import { PolygonTool } from './papertools/polygon.mjs';
import { SelectTool } from './papertools/select.mjs';
import { TransformTool } from './papertools/transform.mjs';
import { RasterTool } from './papertools/raster.mjs';
/**
* Map of tool names to constructors.
* @type {Object.<string, Function>}
*/
const toolConstructors = {
default: DefaultTool,
select: SelectTool,
transform: TransformTool,
style: StyleTool,
rectangle: RectangleTool,
ellipse: EllipseTool,
point: PointTool,
text: PointTextTool,
polygon: PolygonTool,
brush: BrushTool,
wand: WandTool,
linestring: LinestringTool,
ruler: RulerTool,
raster: RasterTool,
};
/**
* Manages the tool layer, tool instances, and mode logic. No DOM.
* AnnotationToolbar is an optional visual wrapper over a toolset.
* @memberof OSDPaperjsAnnotation
* @class
*/
class AnnotationToolset {
/**
* @param {paper.PaperScope} paperScope - The Paper.js scope.
* @param {string[]} [toolNames] - Array of tool names to create. If omitted, all tools are created. 'default' is always included.
*/
constructor(paperScope, toolNames) {
if (toolNames != null && !Array.isArray(toolNames)) {
throw new Error('toolNames must be an array of tool name strings');
}
this.paperScope = paperScope;
this.currentMode = null;
this.setModeTimeout = null;
/** @type {Function|null} Called when mode changes: (mode) => void */
this.onModeChanged = null;
const toolLayer = new paperScope.Layer();
toolLayer.isGeoJSONFeatureCollection = false;
toolLayer.name = 'toolLayer';
toolLayer.applyMatrix = false;
paperScope.project.addLayer(toolLayer);
this.toolLayer = toolLayer;
let toolsToUse = toolNames || Object.keys(toolConstructors);
if (toolsToUse.indexOf('default') === -1) {
toolsToUse = ['default', ...toolsToUse];
}
this.tools = {};
paperScope.activate();
toolsToUse.forEach((t) => {
if (typeof t === 'string') {
if (!toolConstructors[t]) {
console.warn(`The requested tool is invalid: ${t}. No constructor found.`);
return;
}
} else if (!(t instanceof ToolBase)) {
console.warn('Tool must inherit from ToolBase');
return;
}
const ToolConstructor = typeof t === 'string' ? toolConstructors[t] : t;
const toolObj = new ToolConstructor(this.paperScope);
const toolKey = typeof t === 'string' ? t : ToolConstructor.name;
toolObj.toolName = toolKey;
this.tools[toolKey] = toolObj;
toolObj.addEventListener('deactivated', (ev) => {
if (ev.target === this.paperScope.getActiveTool()) {
this.tools.default.activate();
}
});
});
this.tools.default.activate();
const boundSetMode = () => this.setMode();
paperScope.project.on({
'item-replaced': boundSetMode,
'item-selected': boundSetMode,
'item-deselected': boundSetMode,
'item-removed': boundSetMode,
'items-changed': boundSetMode,
});
this._projectHandlers = boundSetMode;
this.setMode();
}
/**
* Compute current mode from selection and optionally deactivate active tool if not enabled for that mode.
* Notifies via onModeChanged so toolbar can update buttons.
*/
setMode() {
this.setModeTimeout && clearTimeout(this.setModeTimeout);
this.setModeTimeout = setTimeout(() => {
this.setModeTimeout = null;
const selection = this.paperScope.findSelectedItems();
const activeTool = this.paperScope.getActiveTool();
const prevMode = this.currentMode;
if (selection.length === 0) {
this.currentMode = 'select';
} else if (selection.length === 1) {
const item = selection[0];
const def = item.annotationItem || {};
let type = def.type;
if (def.subtype) type += ':' + def.subtype;
this.currentMode = type === null ? 'new' : type;
} else {
this.currentMode = 'multiselection';
}
if (activeTool && activeTool.getToolbarControl().isEnabledForMode(this.currentMode) === false) {
activeTool.deactivate(true);
this.tools.default.activate();
}
if (this.onModeChanged) this.onModeChanged(this.currentMode);
const tk = this.paperScope?.annotationToolkit;
if (tk && tk._emitIntegrationEvent && prevMode !== this.currentMode) {
const updated = this.paperScope.getActiveTool();
tk._emitIntegrationEvent('mode-changed', {
from: prevMode ?? null,
to: this.currentMode,
selectionCount: selection.length,
activeTool: { name: updated?.toolName ?? null, tool: updated ?? null },
}, { tool: updated ?? undefined });
}
// In headless usage there may be no AnnotationToolbar to call selectionChanged().
// Ensure the active tool refreshes its cached selection (items + itemToCreate)
// whenever the selection/mode changes.
const updatedActiveTool = this.paperScope.getActiveTool();
if (updatedActiveTool) updatedActiveTool.selectionChanged();
}, 0);
}
/**
* @param {string} name - Tool name (e.g. 'ruler', 'default').
* @returns {ToolBase|null}
*/
getTool(name) {
return this.tools[name] ?? null;
}
/**
* Remove project listeners. Call when toolset is no longer needed.
*/
destroy() {
this.setModeTimeout && clearTimeout(this.setModeTimeout);
this.setModeTimeout = null;
if (this._projectHandlers) {
const h = this._projectHandlers;
const p = this.paperScope.project;
p.off('item-replaced', h);
p.off('item-selected', h);
p.off('item-deselected', h);
p.off('item-removed', h);
p.off('items-changed', h);
this._projectHandlers = null;
}
}
}
export { AnnotationToolset, toolConstructors };