Source: rotationcontrol.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 { OpenSeadragon } from './osd-loader.mjs';
import { ToolBase } from './papertools/base.mjs';
import {PaperOverlay} from './paper-overlay.mjs';
import { paper } from './paperjs.mjs';

/**
 * @class
 * @memberof OSDPaperjsAnnotation
 */
class RotationControlOverlay{
    /**
     * Creates an instance of the RotationControlOverlay.
     *
     * @param {any} viewer - The viewer object.
     */
    constructor(viewer){
        let overlay=this.overlay = new PaperOverlay(viewer,{overlayType:'viewer'})
        let tool = this.tool = new RotationControlTool(this.overlay.paperScope, this);
        this.dummyTool = new this.overlay.paperScope.Tool();//to capture things like mouseMove, keyDown etc (when actual tool is not active)
        this.dummyTool.activate();
        this._mouseNavEnabledAtActivation = true;
        const button = overlay.addViewerButton({
            faIconClass:'fa-rotate',
            tooltip:'Rotate viewer',
            onClick:()=>{
                tool.active ? this.deactivate() : this.activate();
            }
        });
        button.element.querySelector('svg.icon')?.style.setProperty('width','1em');
     
    }
    /**
     * Activates the rotation control.
     */
    activate(){
        this._mouseNavEnabledAtActivation=this.overlay.viewer.isMouseNavEnabled();
        this.tool.activate();
        this.tool.active=true;
        this.overlay.bringToFront();
    }
    /**
     * Deactivates the rotation control.
     */
    deactivate(){
        this.tool.deactivate(true);
        this.dummyTool.activate();
        this.overlay.viewer.setMouseNavEnabled(this._mouseNavEnabledAtActivation);
        this.tool.active=false;
        this.overlay.sendToBack();
    }
    
}

/**
 * @class 
 * @memberof OSDPaperjsAnnotation
 * @extends ToolBase
 * 
 */
class RotationControlTool extends ToolBase{
    /**
     * Creates an instance of the RotationControlTool.
     * @constructor
     * @param {any} paperScope - The paper scope object.
     * @param {any} rotationOverlay - The rotation overlay object.
     */
    constructor(paperScope, rotationOverlay){
        super(paperScope);
        let self=this;
        let bounds = paperScope.view.bounds;
        let widget = new RotationControlWidget(paperScope.view.bounds.center, setAngle, close);

        paperScope.view.on('flip',()=>{
            widget.closeButton.scale(-1, 1, widget.item.bounds.center);
        });

        let viewer = paperScope.overlay.viewer;

        viewer.addHandler('rotate', (ev)=>widget.setCurrentRotation(ev.degrees));
        paperScope.view.on('resize',function(ev){
            let pos = widget.item.position;
            let w = pos.x / bounds.width;
            let h = pos.y / bounds.height;
            bounds = paperScope.view.bounds;//new bounds after the resize
            widget.item.position = new paper.Point(w * bounds.width, h * bounds.height);
        })
        widget.item.visible = false;
        self.project.toolLayer.addChild(widget.item);
        
        //add properties to this.tools so that they properly appear on html
        this.tool.onMouseDown=function(ev){
            
        }
        this.tool.onMouseDrag=function(ev){
            
        }
        this.tool.onMouseMove=function(ev){
            widget.setLineOrientation(ev.point);
        }
        this.tool.onMouseUp = function(){
            
        }
        this.tool.extensions.onKeyDown=function(ev){
            if(ev.key=='escape'){
                rotationOverlay.deactivate();
            }
        }
        this.extensions.onActivate = function(){
            if(widget.item.visible==false){
                widget.item.position=paperScope.view.bounds.center;//reset to center when activated, so that if it gets lost off screen it's easy to recover
            }
            widget.item.visible=true;
            widget.item.opacity = 1;
        }
        this.extensions.onDeactivate = function(finished){
            if(finished){
                widget.item.visible=false;
            }
            widget.item.opacity = 0.3;
        }
        /**
         * Sets the angle of the rotation.
         * @memberof OSDPaperjsAnnotation.RotationControlTool#
         * @param {number} angle - The angle to set.
         * @param {any} pivot - The pivot point for the rotation.
         */
        function setAngle(angle, pivot){
            if(!pivot){
                let widgetCenter = new OpenSeadragon.Point(widget.item.position.x, widget.item.position.y)
                pivot = viewer.viewport.pointFromPixel(widgetCenter);
                
            }
            viewer.viewport.rotateTo(angle, pivot, true);
        }
        function close(){
            rotationOverlay.deactivate();
        }
    }
    
}
export {RotationControlTool};
export {RotationControlOverlay};

/**
 * Creates a rotation control widget.
 * @class
 * @memberof OSDPaperjsAnnotation
 * @param {paper.Point} center - The center point of the widget.
 * @param {Function} setAngle - The function to set the rotation angle.
 * @returns {object} The rotation control widget object.
 */
function RotationControlWidget(center, setAngle, close){
 
    let width = center.x*2;
    let height= center.y*2;
    let radius = Math.min(width/5, height/5, 30);
    let innerRadius = radius * 0.3;

    let baseAngle = new paper.Point(0, -1).angle; //make north the reference direction for 0 degrees (even though normally it would be east)

    //group will contain all the elements of the GUI control
    let group = new paper.Group({insert:false});
    
    //circle is the central region with crosshair and cardinal points
    let circle = new paper.Path.Circle({center:new paper.Point(0,0),radius:radius});
    circle.fillColor = new paper.Color(0,0,0,0.01);//nearly transparent fill so the fill can be clickable
    circle.strokeColor = 'black';
    circle.strokeWidth = 2;
    
    //crosshair to focus on central point of circle
    [0,90,180,270].map(angle=>{
        let crosshair = new paper.Path.Line(new paper.Point(0, innerRadius),new paper.Point(0, radius));
        crosshair.rotate(angle, new paper.Point(0,0));
        crosshair.fillColor = null;
        crosshair.strokeColor = 'black';
        crosshair.strokeWidth = 2;
        group.addChild(crosshair);
    })

    //controls for north, east, south, west    
    let cardinalControls=[0,90,180,270].map(angle=>{
        let rect = new paper.Path.Rectangle(new paper.Point(-innerRadius, 0),new paper.Size(innerRadius*2,-1*(radius+innerRadius*1.5)));
        let control = rect.subtract(circle,{insert:false});
        rect.remove();
        control.rotate(angle, new paper.Point(0,0));
        control.fillColor = new paper.Color(100,100,100,0.5);
        control.strokeColor = 'black';
        control._angle = angle;
        group.addChild(control);
        return control;
        
    })

    //add circle after others so it can capture mouse events
    group.addChild(circle);

    //dot indicating current rotation status of the image
    let currentRotationIndicator = new paper.Path.Circle({center:new paper.Point(0, -radius), radius:innerRadius/1.5});
    currentRotationIndicator.set({fillColor:'yellow',strokeColor:'black',applyMatrix:false});//applyMatrix=false so the rotation property saves current value
    group.addChild(currentRotationIndicator);
    

    //line with arrows indicating that any spot on the image can be grabbed in order to perform rotation
    let rotationLineControl = new paper.Group({applyMatrix:false});
    let arrowControl = new paper.Group({applyMatrix:false});
    
    
    let rcc = new paper.Color(0.3,0.3,0.3,0.8);
    let lineControl = new paper.Path.Line(new paper.Point(0, -innerRadius), new paper.Point(0, -Math.max(width, height)));
    lineControl.strokeColor = rcc;
    lineControl.strokeWidth = 1;
    lineControl.applyMatrix=false;
    rotationLineControl.addChild(lineControl);
    rotationLineControl.addChild(arrowControl);

    let aa=94;
    let ah1 = new paper.Path.RegularPolygon(new paper.Point(-innerRadius*1.2, 0), 3, innerRadius*0.8);
    ah1.rotate(-aa);
    let ah2 = new paper.Path.RegularPolygon(new paper.Point(innerRadius*1.2, 0), 3, innerRadius*0.8);
    ah2.rotate(aa);
    let connector = new paper.Path.Arc(new paper.Point(-innerRadius*1.2, 0),new paper.Point(0, -innerRadius/4),new paper.Point(innerRadius*1.2, 0))
    let connectorbg = connector.clone();
    arrowControl.addChildren([connectorbg,connector,ah1,ah2]);
    arrowControl.fillColor = 'yellow';
    connector.strokeWidth=innerRadius/2;
    connectorbg.strokeWidth = connector.strokeWidth+2;
    connectorbg.strokeColor = rcc;
    ah1.strokeColor = rcc;
    ah2.strokeColor = rcc;
    connector.strokeColor='yellow';
    connector.fillColor=null;

    group.addChild(rotationLineControl);

    // close button
    let closeButton = new paper.Group({insert:false});
    closeButton.addChild(new paper.Path.Circle({radius:innerRadius}));
    closeButton.addChild(new paper.Path.Line(new paper.Point(-innerRadius/2, -innerRadius/2), new paper.Point(innerRadius/2, innerRadius/2)));
    closeButton.addChild(new paper.Path.Line(new paper.Point(innerRadius/2, -innerRadius/2), new paper.Point(-innerRadius/2, innerRadius/2)));
    closeButton.set({fillColor:'red',strokeColor:'black',opacity:0.7});
    closeButton.position = new paper.Point(radius*1.5, -radius*1.5), 
    group.addChild(closeButton);
    
    group.pivot = circle.bounds.center;//make the center of the circle the pivot for the entire  controller
    group.position = center;//set position after adding all children so it is applied to all

    

    //define API

    /**
    * The rotation control widget object.
    *     
    * @memberof OSDPaperjsAnnotation.RotationControlWidget
    * @property {paper.Group} item - The group containing all the elements of the widget.
    * @property {paper.Path.Circle} circle - The central region with crosshair and cardinal points.
    * @property {Array<paper.Path.Rectangle>} cardinalControls - The controls for north, east, south, west.
    * @property {paper.Group} rotationLineControl - The line with arrows indicating the spot for grabbing to perform rotation.
    * @example
    * // Usage example:
    * const rotationControl = new OSDPaperjsAnnotation.RotationControlWidget(centerPoint, setRotationAngle);
    * paper.project.activeLayer.addChild(rotationControl.item);
    * 
    * // Set the current rotation angle
    * rotationControl.setCurrentRotation(45);
    * 
    * // Set the line orientation for control
    * const orientationPoint = new paper.Point(150, 150);
    * rotationControl.setLineOrientation(orientationPoint, true);
    */
    let widget={};
    //add items
    widget.item = group;
    widget.circle = circle;
    widget.cardinalControls = cardinalControls;
    widget.rotationLineControl = rotationLineControl;
    widget.closeButton = closeButton;

    //add API functions
    /**
   * Sets the current rotation angle.
   * @memberof OSDPaperjsAnnotation.RotationControlWidget#
   * @method setCurrentRotation
   * @param {number} angle - The angle to set.
   */
    widget.setCurrentRotation = (angle)=>{
        // console.log('setCurrentRotation',angle);
        currentRotationIndicator.rotate(angle-currentRotationIndicator.rotation, circle.bounds.center)
    };
    /**
     * Sets the orientation of the line control.
     * @memberof OSDPaperjsAnnotation.RotationControlWidget#
     * @method setLineOrientation
     * @param {paper.Point} point - The point representing the orientation.
     * @param {boolean} [makeVisible=false] - Whether to make the control visible.
     */
    widget.setLineOrientation = (point, makeVisible=false)=>{
        let vector = point.subtract(circle.bounds.center);
        let angle = vector.angle - baseAngle;
        let length = vector.length;
        rotationLineControl.rotate(angle - rotationLineControl.rotation, circle.bounds.center);
        rotationLineControl.visible = makeVisible || length > radius+innerRadius*1.5;
        arrowControl.position = new paper.Point(0, -length);
        lineControl.segments[1].point = new paper.Point(0, -length);
    }

    //add intrinsic item-level controls
    cardinalControls.forEach(control=>{
        control.onClick = function(){
            setAngle(control._angle);
        }
    });
    currentRotationIndicator.onMouseDrag=function(ev){
        let dragAngle = ev.point.subtract(circle.bounds.center).angle;
        let angle = dragAngle - baseAngle;
        setAngle(angle);
    }
    arrowControl.onMouseDown=function(ev){
        arrowControl._angleOffset = currentRotationIndicator.rotation - ev.point.subtract(circle.bounds.center).angle;
    }
    arrowControl.onMouseDrag=function(ev){
        let hitResults = this.project.hitTestAll(ev.point).filter(hr=>cardinalControls.includes(hr.item));
        let angle;
        if(hitResults.length>0){
            //we are over a cardinal direction control object; snap the line to that angle
            // angle = -hitResults[0].item._angle + arrowControl._angleOffset;
            ev.point = hitResults[0].item.bounds.center;
        }
        angle = ev.point.subtract(circle.bounds.center).angle + arrowControl._angleOffset;
        setAngle(angle);
        widget.setLineOrientation(ev.point, true);
    }
    // arrowControl.onMouseUp = function(ev){
    //     // console.log('arrow mouseup',ev)
        
    // }
    circle.onMouseDrag=function(ev){
        widget.item.position = widget.item.position.add(ev.delta);
    }

    closeButton.onClick = function(){
        close();
    }

    return widget;
}