Source: papertools/base.mjs

/**
 * OpenSeadragon paperjs overlay plugin based on paper.js
 * @version 0.7.6
 * 
 * Includes additional open source libraries which are subject to copyright notices
 * as indicated accompanying those segments of code.
 * 
 * Original code:
 * Copyright (c) 2022-2026, Thomas Pearce
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * * Neither the name of osd-paperjs-annotation nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

/**
 * The ToolBase Class
 * @class
 * @memberof OSDPaperjsAnnotation
 */ 
class ToolBase{
    /**
     * Create a new instance of ToolBase.
     * @param {paper.PaperScope} paperScope - The Paper.js PaperScope object.
     * @memberof OSDPaperjsAnnotation.ToolBase#
     */    
    constructor(paperScope){

        /**
         * The project interface object containing various properties.
         * If a layer in the current project exists that is named "toolLayer" it will be used by the tool for graphical display
         * , the current active layer will be used as the tool layer. 
         * @memberof OSDPaperjsAnnotation.ToolBase#
         * @function projectInterface
         * @property {Function} getZoom - A function to get the current zoom level.
         * @property {paper.Layer} toolLayer - The layer used by the tool for graphical display.
         * @property {paper.PaperScope} paperScope - The Paper.js PaperScope object.
         * @property {Element} overlay - The overlay element used by the tool.
         */
        this.project ={
            getZoom:()=>paperScope.view.getZoom(),
            toolLayer:paperScope.project.layers.toolLayer || paperScope.project.activeLayer,
            paperScope:paperScope,
            overlay:paperScope.overlay,
        }
        
        let shiftPressed;
        let self=this;

        this._identityMatrix = new paperScope.Matrix();
      
        this.extensions = {
            onActivate:()=>{},
            onDeactivate:()=>{}
        }
        this.tool = new paperScope.Tool();
        
        this.tool._toolObject=this; //TODO is _toolObject actually used, and does it need to be?            
        this.tool.extensions = {
            onKeyUp:()=>{},
            onKeyDown:()=>{},
        }
        this.tool.onKeyDown=function(ev){
            if(!shiftPressed && ev.key==='shift'){
                shiftPressed=true;
                self.onDeactivate();//enable OpenSeadragon event handling for navigation
            }
            this.extensions.onKeyDown(ev);
            self.onKeyDown(ev);
        }
        this.tool.onKeyUp=function(ev){
            if(ev.key=='shift'){
                shiftPressed=false;
                self.onActivate();//start capturing mouse/keyboard events again
            }
            this.extensions.onKeyUp(ev);
            self.onKeyUp(ev);
        }

        this.tool.onMouseDown = ev => {
            this.onMouseDown(ev);
        }
        this.tool.onMouseDrag = ev => {
            this.onMouseDrag(ev);
        }
        this.tool.onMouseMove = ev => {
            this.onMouseMove(ev);
        }
        this.tool.onMouseUp = ev => {
            this.onMouseUp(ev);
        }
        this.listeners = {}
        // Tracks which overlay CSS classes this tool "owns", so they can be cleared
        // centrally even if the tool changes internal modes without full deactivation.
        this._overlayCursorOwnedClasses = new Set();
    }
    getTolerance(pixels, item = null){
        if(!item){
            item = this.item;
        }
        const scalefactor = item?.layer.scaling.x || 1;
        return pixels / scalefactor / this.project.getZoom();
    }
    /**
     * Check if the tool is active.
     * @returns {boolean} True if the tool is active, otherwise false.
     */    
    isActive(){return this._active; }
    activate(){
        this.tool.activate();
        this.onActivate();
    } 
    deactivate(finishToolAction){
        this.onDeactivate(finishToolAction);
    }
    /**
     * Function called when the tool is activated.
     */    
    onActivate(){
        this.captureUserInput(true);
        this.project.overlay.addEventListener('wheel',this.tool.onMouseWheel);
        this.project.toolLayer.bringToFront();
        this.extensions.onActivate();
    }
    /**
     * Function called when the tool is deactivated.
     * @param {boolean} [shouldFinish=false] - Indicates whether the tool should finish its action.
     */    
    onDeactivate(shouldFinish=false){
        this.captureUserInput(false);
        this.project.overlay.removeEventListener('wheel',this.tool.onMouseWheel);
        this.project.toolLayer.sendToBack(); 
        this.extensions.onDeactivate(shouldFinish);
    }
    /**
     * Add an event listener for a specific event type.
     * Remove the listener when no longer needed (e.g. when a UI component unmounts) via removeEventListener
     * to avoid stale callbacks and listener buildup.
     * @param {string} eventType - The type of event to listen for.
     * @param {Function} callback - The callback function to be executed when the event occurs.
     */
    addEventListener(eventType,callback){
        this.listeners[eventType] = this.listeners[eventType]||[];
        this.listeners[eventType].push(callback);
    }
    /**
     * Remove an event listener. Removes the first registered listener for eventType that is === callback.
     * Use the same function reference that was passed to addEventListener. No-op if the callback was never added or was already removed.
     * @param {string} eventType - The type of event.
     * @param {Function} callback - The callback to remove (must be the same reference used in addEventListener).
     */
    removeEventListener(eventType,callback){
        const list = this.listeners[eventType];
        if (!list || !list.length) return;
        const i = list.indexOf(callback);
        if (i !== -1) {
            list.splice(i, 1);
            if (list.length === 0) delete this.listeners[eventType];
        }
    }
    /**
     * Broadcast an event to all registered event listeners for the specified event type.
     * @param {string} eventType - The type of event to broadcast.
     * @param {...*} data - Data to be passed as arguments to the event listeners.
     */    
    broadcast(eventType,...data){
        let listeners = this.listeners[eventType];
        listeners && listeners.forEach(l=>l(...data));
    }

    /**
     * Emit an item lifecycle event from both this tool and the project, so listeners can subscribe
     * either to the tool (tool-specific) or to the project (any tool). Use for item-created, item-updated, item-converted.
     * Emit item-updated when geometry or persisted feature state (e.g. GeoJSON-backed properties, ruler data, text) changes
     * so hosts can save or sync. Use toolkit-only integration events (annotationToolkit._emitIntegrationEvent) for UI or
     * non-persisted affordances (e.g. erase-mode toggled, live preview) where the saved feature is unchanged.
     * @param {string} eventType - One of 'item-created', 'item-updated', 'item-converted'.
     * @param {Object} payload - Must include { item, tool }. When a new part was added, set subpathAdded: true
     *   and include subpath: the Paper item that was added (e.g. Path or Group), so consumers can use it without guessing.
     */
    emitItemEvent(eventType, payload) {
        if (!payload || !payload.item) return;
        if (!payload.tool) payload = { ...payload, tool: this };
        this.broadcast(eventType, payload);
        const project = this.project?.paperScope?.project;
        if (project) project.emit(eventType, payload);
    }

    /**
     * Register overlay cursor CSS classes that this tool may add during interaction.
     * These are considered owned by this tool and can be cleared on deactivate.
     * @param  {...string|string[]} classes
     */
    registerOverlayCursorOwnedClasses(...classes) {
        if (!classes || classes.length === 0) return;
        if (classes.length === 1 && Array.isArray(classes[0])) classes = classes[0];
        const flat = classes.flat ? classes.flat() : classes.reduce((acc, c) => acc.concat(c), []);
        flat.forEach((c) => {
            if (typeof c === 'string' && c) this._overlayCursorOwnedClasses.add(c);
        });
    }

    /**
     * Remove any overlay cursor CSS classes owned by this tool.
     */
    clearOverlayCursorOwnedClasses() {
        if (!this._overlayCursorOwnedClasses || this._overlayCursorOwnedClasses.size === 0) return;
        const classes = Array.from(this._overlayCursorOwnedClasses);
        this.project.overlay.removeClass(...classes);
    }
    
    /**
     * Capture user input to enable or disable OpenSeadragon mouse navigation.
     * @memberof OSDPaperjsAnnotation.ToolBase
     * @inner
     * @param {boolean} [capture=true] - Set to true to capture user input, false to release it.
     */    
    captureUserInput(capture = true) { 
        this.project.overlay.setOSDMouseNavEnabled(!capture);
    };

    // default no-op implementations of tool event handlers
    onMouseDown(){}
    onMouseMove(){}
    onMouseDrag(){}
    onMouseUp(){}
    onKeyDown(){}
    onKeyUp(){}

    
        
}
export {ToolBase};