Source: papertools/pointtext.mjs

/**
 * OpenSeadragon paperjs overlay plugin based on paper.js
 * @version 0.4.13
 * 
 * Includes additional open source libraries which are subject to copyright notices
 * as indicated accompanying those segments of code.
 * 
 * Original code:
 * Copyright (c) 2022-2024, 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.
 * 
 */

import {AnnotationUITool, AnnotationUIToolbarBase} from './annotationUITool.mjs';
import { PointText } from '../paperitems/pointtext.mjs';
import { paper } from '../paperjs.mjs';
import { makeFaIcon } from '../utils/faIcon.mjs';

/**
 * The PointTextTool class empowers the annotation UI with the ability to add point-based text annotations.
 * This class extends the AnnotationUITool and is paired with the PointTextToolbar for interactive control.
 * @class
 * @memberof OSDPaperjsAnnotation
 * 
 */
class PointTextTool extends AnnotationUITool{
/**
 * Initialize the PointTextTool with Paper.js scope, cursor representation, and toolbar controls.
 *
 * @param {paper.PaperScope} paperScope - The Paper.js scope for the tool.
 *
 * @property {paper.PointText} cursor - The visual representation of the text cursor.
 */
    constructor(paperScope){
        super(paperScope);
        
        this.dragging=false;
        
        /**
         * The visual representation of the text cursor.
         * @type {paper.PointText}
         */
        let pointText = new PointText({
            geometry:{
                type:'Point',
                coordinates:[0,0],
                properties:{
                    subtype:'PointText',
                    content:'Click to place',
                }
            },
            properties:{
                label:'Text Tool',
            }
        });
        
        let cursor = this.cursor = pointText.paperItem; cursor.xxy = true;
        cursor.isGeoJSONFeature = false;
        cursor.fillColor='grey';
        cursor.strokeColor='black';
        cursor.strokeWidth = 1;
        cursor.visible=false;
        this.project.toolLayer.addChild(cursor);
        
        pointText.refreshTextOffset();
        cursor.applyRescale();
        
        /**
         * Set the toolbar control for the PointTextTool.
         * This function sets the toolbar control using the provided instance of PointTextToolbar.
         * @private
         * @param {PointTextToolbar} toolbarControl - The toolbar control instance to be set.
         */
        this.setToolbarControl(new PointTextToolbar(this));
        /**
         * Activate event handler for the PointTextTool.
         * This function is called when the tool is activated, and it handles the setup of cursor visibility and interaction behavior.
         * @private
         */
        this.extensions.onActivate = () => {
            if(this.itemToCreate){
                // new item to be created - show the cursor
                this.cursor.visible = true;
                pointText.refreshTextOffset();
                cursor.applyRescale();
            } else if(this.item){
                // modifying an existing item
                this._updateTextInput();
            }
        }
        /**
         * Deactivate event handler for the PointTextTool.
         * This function is called when the tool is deactivated, and it handles cursor visibility and interaction cleanup.
         * @private
         */
        this.extensions.onDeactivate = () => {
            this.project.toolLayer.addChild(this.cursor);
            this.cursor.visible=false;
            this.project.overlay.removeClass('point-tool-grab', 'point-tool-grabbing');
        }
        
    }
    
    onSelectionChanged(){
        this.cursor.visible = !!this.itemToCreate;
        this._updateTextInput();
    }
    onMouseMove(ev){
        this.cursor.position = ev.original.point;
        if(this.item.hitTest(ev.point)){ // for some reason hit-testing needs to be done in transformed coordinates not original
            this.project.overlay.addClass('point-tool-grab');
        }
        else{
            this.project.overlay.removeClass('point-tool-grab');
        }   
    }
    onMouseDown(ev){
        if(this.itemToCreate){
            this.itemToCreate.initializeGeoJSONFeature('Point','PointText');
            this.refreshItems();
            this.item.xxx = true;
            this.item.children[1].content = this.toolbarControl.getValue();
            this.item.position=ev.point;
            this.cursor.visible=false;
            this.toolbarControl.updateInstructions('Point:PointText');
        }
        else{
            if(this.item && this.item.hitTest(ev.point)){ // for some reason hit-testing needs to be done in transformed coordinates not original
                this.dragging=true;
                this.project.overlay.addClass('point-tool-grabbing')
            }
        }
    }
    onMouseDrag=function(ev){
        if(this.dragging){
            this.item && (this.item.position = this.item.position.add(ev.delta))
        }
    }
    onMouseUp(){
        this.dragging=false;
        this.project.overlay.removeClass('point-tool-grabbing');
    }
    

    _updateTextInput(){
        let text = (this.item && this.item.annotationItem.subtype=='PointText') ? this.item.children[1].content : '';
        this.toolbarControl.setItemText(text);
        this.cursor.children[1].content = text.length ? text : this.toolbarControl.input.getAttribute('placeholder');
    }
}
export{PointTextTool};


/**
 * The PointTextToolbar class enhances the PointTextTool by providing an interactive toolbar for text annotation controls.
 * This class extends the AnnotationUIToolbarBase to manage the toolbar's behavior.
 * @class
 * @memberof OSDPaperjsAnnotation.PointTextTool
 * 
 * 
 */

class PointTextToolbar extends AnnotationUIToolbarBase{
    /**
     * Create a new instance of the PointTextToolbar, complementing the associated PointTextTool.
     *
     * @param {PointTextTool} tool - The corresponding PointTextTool instance.
     */
    constructor(tool){
        super(tool);
        let self = this;

        const i = makeFaIcon('fa-font');
        this.button.configure(i,'Text Tool');

        this.instructions = document.createElement('span');
        this.instructions.classList.add('instructions');
        this.dropdown.appendChild(this.instructions);

        this.input = document.createElement('input');
        this.input.setAttribute('type','text');
        this.input.setAttribute('placeholder', 'Enter text');
        this.dropdown.appendChild(this.input);
        this.input.addEventListener('input', function(){
            let value = self.getValue();
            if(self.tool.item && self.tool.item.annotationItem.subtype=='PointText'){
                self.tool.item.children[1].content = value;
            }
            self.tool.cursor.children[1].content = value;
        })
        
        this.input.dispatchEvent(new Event('input'));
    }
    /**
     * Update the input element's text content.
     *
     * @param {string} text - The text to be set in the input.
     */
    setItemText(text){
        this.input.value = text;
    }
    /**
     * Retrieve the current value from the input element.
     *
     * @returns {string} The value from the input.
     */
    getValue(){
        let input = this.input;
        return input.value.trim() || input.getAttribute('placeholder');
    }
    /**
     * Determine if the toolbar is enabled for the given annotation mode.
     *
     * @param {string} mode - The current annotation mode.
     * @returns {boolean} True if the toolbar is enabled, otherwise false.
     */
    isEnabledForMode(mode){
        this.updateInstructions(mode);
        return ['new','Point:PointText'].includes(mode);
    }
    
    /**
     * Update the instructional text based on the current annotation mode.
     *
     * @param {string} mode - The current annotation mode.
     */
    updateInstructions(mode){
        const text = mode=='new' ? 'Click to place' : mode=='Point:PointText' ? 'Drag to reposition' : '???';
        this.instructions.innerHTML = text;
    }
}