001/*
002 *   VirtualSphere -- A class that implements the Virtual Sphere or Arc Ball algorithm for 3D rotation.
003 *   
004 *   Copyright (C) 2001-2023 by Joseph A. Huwaldt.
005 *   All rights reserved.
006 *   
007 *   This library is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *   
012 *   This library is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 *   Lesser General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public License
018 *   along with this program; if not, write to the Free Software
019 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
020 *   Or visit:  http://www.gnu.org/licenses/lgpl.html
021 */
022package jahuwaldt.j3d;
023
024import org.jogamp.java3d.exp.swing.JCanvas3D;
025import org.jogamp.java3d.utils.behaviors.mouse.*;
026import java.awt.*;
027import java.awt.event.ComponentAdapter;
028import java.awt.event.ComponentEvent;
029import java.awt.event.MouseEvent;
030import java.awt.event.MouseWheelEvent;
031import java.awt.image.BufferedImage;
032import static java.lang.Math.*;
033import java.util.ArrayList;
034import java.util.Enumeration;
035import java.util.Iterator;
036import java.util.List;
037import static java.util.Objects.requireNonNull;
038import org.jogamp.java3d.*;
039import org.jogamp.vecmath.*;
040
041/**
042 * VirtualSphere is a Java3D behavior object that lets users control the orientation,
043 * translation and scale of an object via a mouse. This class implements the virtual
044 * sphere algorithm for 3D rotation using a 2D input device. This is a much simpler
045 * implementation than that described in the reference paper. This is also known by as the
046 * "virtual track ball", "cue ball", or "arc ball" interface. This implementation is
047 * designed to work with Java3D.
048 * <p>
049 * Reference: Chen, Michael, Mountford, S. Jay, Sellen, Abigail, "A Study in Interactive
050 * 3-D Rotation Using 2-D Control Devices," ACM Siggraph '88 Proceedings, Volume 22,
051 * Number 4, August 1988.
052 * </p>
053 * <p>
054 * To use this utility, first create a transform group that this mouse input behavior will
055 * operate on. Then,
056 * <blockquote><pre>
057 *
058 *   VirtualSphere behavior = new VirtualSphere(canvas3D);
059 *   behavior.setTransformGroup(objTrans);
060 *   objTrans.addChild(behavior);
061 *   behavior.setSchedulingBounds(bounds);
062 *
063 *</pre></blockquote>
064 * The above code will add the virtual sphere behavior to the transform group. The user
065 * can then rotate, translate and scale any object attached to the objTrans. This class
066 * implements mouse rotation, translation, scaling and mouse wheel scaling actions.
067 * </p>
068 *
069 * <p> Modified by: Joseph A.Huwaldt </p>
070 * 
071 * @author Joseph A. Huwaldt, Date: Feburary 19, 2001
072 * @version June 4, 2023
073 */
074public class VirtualSphere extends MouseBehavior {
075
076    //  The angular size in degrees of the virtual sphere overlay cue "cross-hair".
077
078    private static final int CROSSHAIR_SIZE = 20;
079
080    //  Temporary storage objects used in calculations.
081    private final Vector3d _op = new Vector3d();
082    private final Vector3d _oq = new Vector3d();
083    private final Vector3d _av = new Vector3d();
084    private final AxisAngle4d _a1 = new AxisAngle4d();
085    private final Transform3D _mouseMtx = new Transform3D();
086    private final Vector3d _translation = new Vector3d();
087
088    //  The zoom scale factor being used.
089    private double _zoomScale = 1;
090
091    //  Scale factors used for translation and for zooming.
092    private double _xTransFactor = .001;
093    private double _yTransFactor = .001;
094    private double _zoomFactor = 0.04;
095    private double _wheelZoomFactor = 0.1;
096
097    //  The point on the Canvas3D that represents the center of the canvas.
098    //  This is the center of the virutal sphere.
099    private final Point _cueCenter = new Point();
100
101    //  The radius of the virtual sphere in pixels on the canvas.
102    private int _cueRadius;
103
104    //  The canvas this virtual sphere is associated with.
105    private final Component _canvas;
106
107    //  Used to track the mouse position over the canvas.
108    private Point _prevMouse;
109
110    //  An Overlay that displays a screen representation of the virtual sphere.
111    private VSOverlay _overlay = null;
112
113    //  The color used to render the overlay graphics.
114    private Paint _overlayPaint = new Color(Color.BLACK.getRed() / 255F, Color.BLACK.getGreen() / 255F,
115            Color.BLACK.getBlue() / 255F, 0.25F);
116
117    private MouseBehaviorCallback _callback = null;
118
119    //  A list of TransformChangeListeners.
120    private final List<TransformChangeListener> _transformListeners = new ArrayList();
121
122    //  Flag indicating that the view is currently being rotated.
123    private boolean _isRotating = false;
124
125    /**
126     * Creates a <code>VirtualSphere</code> behavior associated with the specified canvas.
127     *
128     * @param canvas The Java3D canvas to associate this virtual sphere with.
129     */
130    public VirtualSphere(Canvas3D canvas) {
131        super(0);
132        _canvas = requireNonNull(canvas, "canvas == null");
133
134        //  Set the initial size for the virtual sphere.
135        updateCueSize(canvas);
136
137        //  Add a component resize listener to the canvas.
138        canvas.addComponentListener(new ComponentAdapter() {
139            @Override
140            public void componentResized(ComponentEvent e) {
141                updateCueSize(e.getComponent());
142            }
143        });
144
145    }
146
147    /**
148     * Creates a <code>VirtualSphere</code> behavior associated with the specified canvas.
149     *
150     * @param canvas The Java3D canvas to associate this virtual sphere with.
151     */
152    public VirtualSphere(JCanvas3D canvas) {
153        super(0);
154        _canvas = requireNonNull(canvas, "canvas == null");
155
156        //  Set the initial size for the virtual sphere.
157        updateCueSize(canvas);
158
159        //  Add a component resize listener to the canvas.
160        canvas.addComponentListener(new ComponentAdapter() {
161            @Override
162            public void componentResized(ComponentEvent e) {
163                updateCueSize(e.getComponent());
164            }
165        });
166
167    }
168
169    /**
170     * Initializes the behavior.
171     */
172    @Override
173    public void initialize() {
174        super.initialize();
175        if ((this.flags & INVERT_INPUT) == INVERT_INPUT) {
176            this.invert = true;
177            _xTransFactor *= -1;
178            _yTransFactor *= -1;
179            _zoomFactor *= -1;
180            _wheelZoomFactor *= -1;
181        }
182    }
183
184    /**
185     * Return the x-axis translation multiplier.
186     *
187     * @return The x-axis translation multiplier.
188     */
189    public double getXTranslationFactor() {
190        return _xTransFactor;
191    }
192
193    /**
194     * Return the y-axis translation multiplier.
195     *
196     * @return The y-axis translation multiplier.
197     */
198    public double getYTranslationFactor() {
199        return _yTransFactor;
200    }
201
202    /**
203     * Return the mouse zoom multiplier.
204     *
205     * @return The mouse zoom multiplier.
206     */
207    public double getMouseZoomFactor() {
208        return _zoomFactor;
209    }
210
211    /**
212     * Return the scroll wheel zoom multiplier.
213     *
214     * @return The scroll wheel zoom multiplier.
215     */
216    public double getWheelZoomFactor() {
217        return _wheelZoomFactor;
218    }
219
220    /**
221     * Set the X-axis and Y-axis translation multiplier factor.
222     *
223     * @param factor The factor to set the X & Y axis translation multiplier to.
224     */
225    public void setTranslationFactor(double factor) {
226        _xTransFactor = _yTransFactor = factor;
227    }
228
229    /**
230     * Set the X-axis and Y-axis translation multiplier with xFactor and yFactor
231     * respectively.
232     *
233     * @param xFactor The X-axis translation multiplier factor.
234     * @param yFactor The Y-axis translation multiplier factor.
235     */
236    public void setTranslationFactor(double xFactor, double yFactor) {
237        _xTransFactor = xFactor;
238        _yTransFactor = yFactor;
239    }
240
241    /**
242     * Set the mouse zoom multiplier factor.
243     *
244     * @param factor The mouse zoom multiplier.
245     */
246    public void setMouseZoomFactor(double factor) {
247        _zoomFactor = factor;
248    }
249
250    /**
251     * Set the scroll wheel zoom multiplier with factor.
252     *
253     * @param factor The scroll wheel zoom multiplier.
254     */
255    public void setWheelZoomFactor(double factor) {
256        _wheelZoomFactor = factor;
257    }
258
259    /**
260     * Return the zoom scale. This is the scale factor applied to the model to zoom in/out
261     * before the model is displayed.
262     *
263     * @return The zoom scale.
264     */
265    public double getZoomScale() {
266        return _zoomScale;
267    }
268
269    /**
270     * Set the zoom scale. This is the scale factor applied to the model to zoom in/out
271     * before the model is displayed. The value must be > 0 or nothing happens.
272     *
273     * @param zoomScale The zoom scale to set.
274     */
275    public void setZoomScale(double zoomScale) {
276        if (zoomScale <= 0)
277            return;
278
279        //  Get the current transform.
280        this.transformGroup.getTransform(this.currXform);
281
282        //  Remove the current zoom transformation.
283        doZoom(this.currXform, 1.0 / _zoomScale);
284
285        //  Store the new zoom scale.
286        _zoomScale = zoomScale;
287
288        //  Zoom the transformation matrix.
289        doZoom(this.currXform, zoomScale);
290
291        //  Update the transform.
292        this.transformGroup.setTransform(this.currXform);
293
294        //  Notify any transform listeners.
295        fireTransformChanged(TransformChangeEvent.Type.ZOOM, this.currXform);
296    }
297
298    /**
299     * Set the center of rotation on the model for the virtual sphere. The transform will
300     * be adjusted to move center the specified point on the screen and the virtual sphere
301     * will rotate about it.
302     *
303     * @param rotationCenter The point about which the model should be rotated in virtual
304     *                       world coordinates.
305     */
306    public void setRotationCenter(Point3d rotationCenter) {
307        requireNonNull(rotationCenter, "rotationCenter == null");
308
309        //  Get the current transform.
310        this.transformGroup.getTransform(this.currXform);
311
312        //  Get the current translation.
313        this.currXform.get(_translation);
314
315        //  Get the change in rotation center.
316        double dx = rotationCenter.x;
317        double dy = rotationCenter.y;
318        double dz = rotationCenter.z;
319
320        //  Change the translation to point to the new location.
321        _translation.x -= dx;
322        _translation.y -= dy;
323        _translation.z -= dz;
324        this.currXform.setTranslation(_translation);
325
326        //  Update the transform.
327        this.transformGroup.setTransform(this.currXform);
328
329        //  Notify any transform listeners.
330        fireTransformChanged(TransformChangeEvent.Type.TRANSLATE, this.currXform);
331    }
332
333    /**
334     * Set the center of rotation on the model and the zoom scale for the virtual sphere.
335     * The transform will be adjusted to move center the specified point on the screen and
336     * the virtual sphere will rotate about it. The zoom scale will also be adjusted.
337     *
338     * @param rotationCenter The point about which the model should be rotated in virtual
339     *                       world coordinates.
340     * @param zoomScale      The scale factor used to zoom the model.
341     * @see #setZoomScale
342     */
343    public void setRotationCenter(Point3d rotationCenter, double zoomScale) {
344        requireNonNull(rotationCenter, "rotationCenter == null");
345
346        //  Get the current transform.
347        this.transformGroup.getTransform(this.currXform);
348
349        //  Get the change in rotation center.
350        double dx = rotationCenter.x;
351        double dy = rotationCenter.y;
352        double dz = rotationCenter.z;
353
354        //  Get the current translation.
355        this.currXform.get(_translation);
356
357        //  Change the translation to point to the new location.
358        _translation.x -= dx;
359        _translation.y -= dy;
360        _translation.z -= dz;
361        this.currXform.setTranslation(_translation);
362
363        //  Remove the current zoom transformation.
364        doZoom(this.currXform, 1.0 / _zoomScale);
365
366        //  Store the new zoom scale.
367        _zoomScale = zoomScale;
368
369        //  Apply the new zoom scale.
370        doZoom(this.currXform, zoomScale);
371
372        //  Update the transform.
373        this.transformGroup.setTransform(this.currXform);
374
375        //  Notify any transform listeners.
376        fireTransformChanged(TransformChangeEvent.Type.TRANSLATE, this.currXform);
377        fireTransformChanged(TransformChangeEvent.Type.ZOOM, this.currXform);
378    }
379
380    /**
381     * Change the current rotation matrix to the one specified.
382     *
383     * @param rotationMatrix The rotation matrix to be used.
384     */
385    public void setRotation(Matrix3d rotationMatrix) {
386        requireNonNull(rotationMatrix, "rotationMatrix == null");
387
388        //  Get the current transform.
389        this.transformGroup.getTransform(this.currXform);
390
391        //  Remove the current rotation.
392        Matrix3d rotation = new Matrix3d();
393        this.currXform.get(rotation);
394        this.transformX.set(rotation);
395        this.transformX.invert();
396        this.currXform.mul(this.transformX, this.currXform);
397
398        //  Change to the new orientation.
399        this.transformX.set(rotationMatrix);
400        this.currXform.mul(this.transformX, this.currXform);
401
402        //  Update the transform.
403        this.transformGroup.setTransform(this.currXform);
404
405        //  Notify any transform listeners.
406        fireTransformChanged(TransformChangeEvent.Type.ROTATE, this.currXform);
407    }
408
409    /**
410     * Add a {@link TransformChangeListener} to this object.
411     *
412     * @param listener The listener to add.
413     */
414    public void addTransformChangeListener(TransformChangeListener listener) {
415        _transformListeners.add(requireNonNull(listener, "listener == null"));
416    }
417
418    /**
419     * Remove a {@link TransformChangeListener} from this object.
420     *
421     * @param listener The listener to add.
422     */
423    public void removeTransformChangeListener(TransformChangeListener listener) {
424        _transformListeners.remove(requireNonNull(listener, "listener == null"));
425    }
426
427    /**
428     * Returns an {@link BGFGImage} for use in an {@link BGFGCanvas3D} that displays a
429     * visual representation of the Virtual Sphere for user feedback. This overlay is
430     * specific to the canvas that was used to create this virtual sphere and should not
431     * be used with any other canvas.
432     *
433     * @return The Virtual Sphere feedback image.
434     */
435    public BGFGImage getFeedbackOverlay() {
436        if (_overlay == null)
437            _overlay = new VSOverlay();
438        return _overlay;
439    }
440
441    /**
442     * Returns the <code>Paint</code> used to render the virtual sphere overlay graphics.
443     *
444     * @return The Paint used to render the virtual sphere overlay graphics.
445     */
446    public Paint getOverlayPaint() {
447        return _overlayPaint;
448    }
449
450    /**
451     * Sets the <code>Paint</code> used to render the virtual sphere overlay graphics.
452     *
453     * @param paint The paint to use for the virtual sphere overlay graphics.
454     */
455    public void setOverlayPaint(Paint paint) {
456        requireNonNull(paint, "paint == null");
457        _overlayPaint = paint;
458    }
459
460    /**
461     * An overlay that displays a visual representation of the virtual sphere for user
462     * feedback.
463     */
464    private final class VSOverlay implements BGFGImage {
465
466        private BufferedImage _bufIm = null;
467        private Graphics2D _g2d = null;
468        private int _xcue = 0;
469        private int _ycue = 0;
470        private int _cueDiameter = 0;
471
472        public VSOverlay() {
473            //  Initialize the image buffer.
474            updateBufferSize(_canvas);
475        }
476
477        /**
478         * Updates the size of the image buffer if the canvas size has changed.
479         */
480        public synchronized void updateBufferSize(Component canvas) {
481            int width = canvas.getWidth();
482            int height = canvas.getHeight();
483
484            if (width == 0 || height == 0) {
485                _cueDiameter = 0;
486                _xcue = 0;
487                _ycue = 0;
488
489            } else {
490                int newDiameter = min(width - 20, height - 20);
491                if (newDiameter != _cueDiameter) {
492                    _cueDiameter = newDiameter;
493                    ++newDiameter;
494                    //  Note:  Using BufferedImage.TYPE_4BYTE_ABGR is faster, but then the
495                    //  canvas can not be resized.  This workaround seems to solve that problem
496                    //  at the price of a little performance.
497                    _bufIm = new BufferedImage(newDiameter, newDiameter, BufferedImage.TYPE_INT_ARGB);
498                    _g2d = _bufIm.createGraphics();
499                }
500                _xcue = (width - _cueDiameter) / 2;
501                _ycue = (height - _cueDiameter) / 2;
502            }
503        }
504
505        /**
506         * Returns the image overlay for use by an OverlayCanvas3D. The feedback image is
507         * rendered here.
508         */
509        @Override
510        public synchronized BufferedImage getImage() {
511            if (_isRotating) {
512                //  Clear the buffer and set the pen color.
513                clearSurface(_g2d);
514
515                //  Draw a circle around the perimeter of the virtual sphere.
516                _g2d.drawOval(0, 0, _cueDiameter, _cueDiameter);
517
518                //  Draw a cross-hair where the mouse touches the virtual sphere.
519                drawMouseCue(_g2d);
520
521                return _bufIm;
522            }
523
524            return null;
525        }
526
527        /**
528         * Clears the buffered image to be 100% transparent.
529         */
530        private void clearSurface(Graphics2D drawg2d) {
531            drawg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
532            drawg2d.fillRect(0, 0, _cueDiameter + 1, _cueDiameter + 1);
533            drawg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
534            drawg2d.setPaint(_overlayPaint);
535        }
536
537        /**
538         * Draws the mouse cue symbol.
539         */
540        private void drawMouseCue(Graphics2D g2d) {
541            //  Get the mouse location relative to the cue center.
542            int mouseX = _prevMouse.x - _cueCenter.x;
543            int mouseY = _prevMouse.y - _cueCenter.y;
544            int mouseX2 = mouseX * mouseX;
545            int mouseY2 = mouseY * mouseY;
546            int cueRadius2 = _cueRadius * _cueRadius;
547
548            // Draw the mouse cue if we are over the sphere.
549            if (mouseX2 + mouseY2 < cueRadius2) {
550
551                // Draw the vertical cue line.
552                if (abs(mouseX) > 2) {
553                    //  Draw a vertical elliptical arc through the mouse point.
554                    double a = sqrt(mouseX2 / (1 - mouseY2 / (double)cueRadius2));
555                    double newMouseX = mouseX * _cueRadius / a;
556                    int angle = (int)(-atan2(mouseY, newMouseX) * 180 / PI);
557
558                    //  Draw a piece of an ellipse.
559                    int x = _cueRadius - (int)a;
560                    g2d.drawArc(x, 0, (int)(a * 2), _cueDiameter, angle - CROSSHAIR_SIZE / 2, CROSSHAIR_SIZE);
561
562                } else {
563                    //  Mouse X near zero is a special case, just draw a vertical line.
564                    double vy = mouseY / (double)_cueRadius;
565                    double vy2 = vy * vy;
566                    double vz = sqrt(1 - vy2);
567                    double angle = atan2(vy, vz) - CROSSHAIR_SIZE * PI / 180 / 2;
568                    double length = sqrt(vy2 + vz * vz) * _cueRadius;
569                    int yl = (int)(length * sin(angle)) + _cueRadius;
570                    int yh = (int)(length * sin(angle + CROSSHAIR_SIZE * PI / 180)) + _cueRadius;
571
572                    //  Render the line.
573                    int x = _prevMouse.x - _xcue;
574                    g2d.drawLine(x, yl, x, yh);
575                }
576
577                //  Draw the horizontal cue line.
578                if (abs(mouseY) > 2) {
579                    //  Draw a horizontal elliptical arc through the mouse point.
580                    double a = sqrt(mouseY2 / (1 - mouseX2 / (double)cueRadius2));
581                    double newMouseY = mouseY * _cueRadius / a;
582                    int angle = (int)(-atan2(newMouseY, mouseX) * 180 / PI);
583
584                    //  Draw a piece of an ellipse.
585                    int y = _cueRadius - (int)a;
586                    g2d.drawArc(0, y, _cueDiameter, (int)(a * 2), angle - CROSSHAIR_SIZE / 2, CROSSHAIR_SIZE);
587
588                } else {
589                    //  Mouse Y near zero is a special case, just draw a horizontal line.
590                    double vx = mouseX / (double)_cueRadius;
591                    double vx2 = vx * vx;
592                    double vz = sqrt(1 - vx2);
593                    double angle = atan2(vx, vz) - CROSSHAIR_SIZE * PI / 180 / 2;
594                    double length = sqrt(vx2 + vz * vz) * _cueRadius;
595                    int xl = (int)(length * sin(angle)) + _cueRadius;
596                    int xh = (int)(length * sin(angle + CROSSHAIR_SIZE * PI / 180)) + _cueRadius;
597
598                    //  Render the line.
599                    int y = _prevMouse.y - _ycue;
600                    g2d.drawLine(xl, y, xh, y);
601                }
602            }
603        }
604
605        /**
606         * Returns the X coordinate of the upper left corner of the image.
607         */
608        @Override
609        public int getImageX() {
610            return _xcue;
611        }
612
613        /**
614         * Returns the Y coordinate of the upper left corner of the image.
615         */
616        @Override
617        public int getImageY() {
618            return _ycue;
619        }
620    }
621
622    /**
623     * Method that updates the cue center and cue radius based on the current size of the
624     * canvas.
625     */
626    private void updateCueSize(Component canvas) {
627        int width = canvas.getWidth();
628        int height = canvas.getHeight();
629
630        if (width == 0 || height == 0) {
631            _cueCenter.x = 0;
632            _cueCenter.y = 0;
633            _cueRadius = 0;
634
635        } else {
636            _cueCenter.x = width / 2;
637            _cueCenter.y = height / 2;
638            _cueRadius = min(width - 20, height - 20) / 2;
639        }
640
641        if (_overlay != null)
642            _overlay.updateBufferSize(canvas);
643    }
644
645    /**
646     * Processes the MouseBehavior stimulus. This method is invoked if the Behavior's
647     * wakeup criteria are satisfied and an active ViewPlatform's activation volume
648     * intersects with the Behavior's scheduling region.
649     *
650     * @param criteria An enumeration of triggered wakeup criteria for this behavior.
651     */
652    @Override
653    @SuppressWarnings("SynchronizeOnNonFinalField")
654    public void processStimulus(Iterator<WakeupCriterion> criteria) {
655        requireNonNull(criteria, "criteria == null");
656        WakeupCriterion wakeup;
657        AWTEvent[] events;
658        MouseEvent evt;
659
660        while (criteria.hasNext()) {
661            wakeup = criteria.next();
662            if (wakeup instanceof WakeupOnAWTEvent) {
663                events = ((WakeupOnAWTEvent)wakeup).getAWTEvent();
664                if (events.length > 0) {
665                    evt = (MouseEvent)events[events.length - 1];
666                    doProcess(evt);
667                }
668            } else if (wakeup instanceof WakeupOnBehaviorPost) {
669                while (true) {
670                    // access to the queue must be synchronized
671                    synchronized (mouseq) {
672                        if (mouseq.isEmpty())
673                            break;
674                        evt = (MouseEvent)mouseq.remove(0);
675                        // consolidate MOUSE_DRAG events
676                        while ((evt.getID() == MouseEvent.MOUSE_DRAGGED)
677                                && !mouseq.isEmpty()
678                                && (((MouseEvent)mouseq.get(0)).getID() == MouseEvent.MOUSE_DRAGGED)) {
679                            evt = (MouseEvent)mouseq.remove(0);
680                        }
681                        // consolidate MOUSE_WHEEL events
682                        while ((evt.getID() == MouseEvent.MOUSE_WHEEL)
683                                && !mouseq.isEmpty()
684                                && (((MouseEvent)mouseq.get(0)).getID() == MouseEvent.MOUSE_WHEEL)) {
685                            evt = (MouseEvent)mouseq.remove(0);
686                        }
687                    }
688                    doProcess(evt);
689                }
690            }
691
692        }
693        wakeupOn(mouseCriterion);
694    }
695
696    /**
697     * Processes the provided mouse event doing the right thing for each type.
698     *
699     * @param evt The mouse event to be processed.
700     */
701    protected void doProcess(MouseEvent evt) {
702
703        processMouseEvent(requireNonNull(evt));
704        int id = evt.getID();
705        if ((id == MouseEvent.MOUSE_WHEEL)) {
706            //  Mouse wheel event.
707            int units = 0;
708
709            MouseWheelEvent wheelEvent = (MouseWheelEvent)evt;
710            if (wheelEvent.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL)
711                units = wheelEvent.getUnitsToScroll();
712
713            handleWheelZoom(units);
714
715        } else if (((this.buttonPress) && ((this.flags & MANUAL_WAKEUP) == 0))
716                || ((this.wakeUp) && ((this.flags & MANUAL_WAKEUP) != 0))) {
717            //  Mouse drag with mouse button pressed event.
718            if (id == MouseEvent.MOUSE_DRAGGED) {
719                if (!evt.isMetaDown() && !evt.isAltDown() && !evt.isControlDown()) {
720                    _isRotating = true;
721                    handleMouseRotate(evt);
722
723                } else if (!evt.isAltDown() && evt.isMetaDown())
724                    handleMouseTranslate(evt);
725
726                else if (evt.isAltDown() && !evt.isMetaDown())
727                    handleMouseZoom(evt);
728
729            } else if (id == MouseEvent.MOUSE_PRESSED) {
730                _prevMouse = evt.getPoint();
731            }
732
733        } else if (id == MouseEvent.MOUSE_RELEASED) {
734            _isRotating = false;
735
736            //  Redraw the overlay if the mouse is released.
737            if (_overlay != null)
738                fireTransformChanged(TransformChangeEvent.Type.ROTATE, this.currXform);
739        }
740    }
741
742    /**
743     * Handles the user dragging the mouse across the canvas to rotate the transform.
744     */
745    private void handleMouseRotate(MouseEvent evt) {
746        Point newMouse = evt.getPoint();
747
748        if (newMouse.x != _prevMouse.x || newMouse.y != _prevMouse.y) {
749            if (!this.reset) {
750                //  Get the current transform.
751                this.transformGroup.getTransform(this.currXform);
752
753                //  Calculate incremental transform due to mouse movement.
754                makeRotationMtx(_prevMouse, newMouse, _cueCenter, _cueRadius, _mouseMtx);
755
756                //  Rotate to the new orientation.
757                this.currXform.mul(_mouseMtx, this.currXform);
758
759                //  Update the transform.
760                this.transformGroup.setTransform(this.currXform);
761
762                //  Notify any transform listeners.
763                fireTransformChanged(TransformChangeEvent.Type.ROTATE, this.currXform);
764
765            } else {
766                this.reset = false;
767            }
768
769            //  Update the mouse position.
770            _prevMouse = newMouse;
771        }
772
773    }
774
775    /**
776     * Handles the user dragging the mouse across the canvas to translate the transform.
777     */
778    private void handleMouseTranslate(MouseEvent evt) {
779        Point newMouse = evt.getPoint();
780
781        if (newMouse.x != _prevMouse.x || newMouse.y != _prevMouse.y) {
782            int dx = newMouse.x - _prevMouse.x;
783            int dy = newMouse.y - _prevMouse.y;
784
785            if ((!this.reset) && ((abs(dy) < 50) && (abs(dx) < 50))) {
786                //  Get the current transform.
787                this.transformGroup.getTransform(this.currXform);
788
789                //  Create a translation matrix.
790                _translation.x = dx * _xTransFactor;
791                _translation.y = -dy * _yTransFactor;
792                _translation.z = 0;
793                this.transformX.set(_translation);
794
795                //  Translate to the new orientation.
796                if (this.invert) {
797                    this.currXform.mul(this.currXform, this.transformX);
798                } else {
799                    this.currXform.mul(this.transformX, this.currXform);
800                }
801
802                //  Update the transform.
803                this.transformGroup.setTransform(this.currXform);
804
805                //  Notify any transform listeners.
806                fireTransformChanged(TransformChangeEvent.Type.TRANSLATE, this.currXform);
807
808            } else {
809                this.reset = false;
810            }
811
812            //  Update the mouse position.
813            _prevMouse = newMouse;
814        }
815
816    }
817
818    /**
819     * Handles the user dragging the mouse across the canvas to zoom the transform.
820     */
821    private void handleMouseZoom(MouseEvent evt) {
822        Point newMouse = evt.getPoint();
823
824        if (newMouse.y != _prevMouse.y) {
825
826            if (!this.reset) {
827                int dy = newMouse.y - _prevMouse.y;
828
829                //  Get the current transform.
830                this.transformGroup.getTransform(this.currXform);
831
832                //  Determine the zoom scale amount.
833                double factor = _zoomFactor * dy + 1;
834                if (factor < 0.5)
835                    factor = 0.5;
836                else if (factor > 2)
837                    factor = 2;
838
839                //  Zoom the transformation matrix.
840                doZoom(this.currXform, factor);
841                _zoomScale *= factor;
842
843                //  Update the transform.
844                this.transformGroup.setTransform(this.currXform);
845
846                //  Notify any transform listeners.
847                fireTransformChanged(TransformChangeEvent.Type.ZOOM, this.currXform);
848
849            } else {
850                this.reset = false;
851            }
852
853            //  Update the mouse position.
854            _prevMouse = newMouse;
855        }
856
857    }
858
859    /**
860     * Handles the user rolling the mouse scroll wheel.
861     *
862     * @param units The amount that the scroll wheel has been moved.
863     */
864    private void handleWheelZoom(int units) {
865        if (units != 0) {
866            if (!this.reset) {
867                //  Get the current transform.
868                this.transformGroup.getTransform(this.currXform);
869
870                //  Determine the zoom screen Z translation amount.
871                double factor = _wheelZoomFactor * units + 1;
872                if (factor < 0.5)
873                    factor = 0.5;
874                else if (factor > 2)
875                    factor = 2;
876
877                //  Zoom the transformation matrix.
878                doZoom(this.currXform, factor);
879                _zoomScale *= factor;
880
881                //  Update the transform.
882                this.transformGroup.setTransform(this.currXform);
883
884                //  Notify any transform listeners.
885                fireTransformChanged(TransformChangeEvent.Type.ZOOM, this.currXform);
886
887            } else {
888                this.reset = false;
889            }
890        }
891    }
892
893    /**
894     * Method that modifies the input transform by applying the specified scale factor to
895     * the specified transform.
896     *
897     * @param t3d       The transform to be scaled to represent the zoom. This transform
898     *                  is modified in place for output.
899     * @param zoomScale The scale factor to apply to the transform. Value must be > 0 or
900     *                  it will be ignored.
901     */
902    private void doZoom(Transform3D t3d, double zoomScale) {
903        if (zoomScale <= 0)
904            return;
905
906        //  Create a transform representing the uniform scale.
907        this.transformX.set(zoomScale);
908
909        //  Zoom to the new scale.
910        if (this.invert) {
911            t3d.mul(t3d, this.transformX);
912        } else {
913            t3d.mul(this.transformX, t3d);
914        }
915
916    }
917
918    /**
919     * Fire a transform changed event to notify any listeners of changes in the
920     * transformation.
921     */
922    private void fireTransformChanged(TransformChangeEvent.Type type, Transform3D transform) {
923        //  First call the transformChanged() method.
924        transformChanged(transform);
925
926        //  Then notify the callback if it exists.
927        if (_callback != null) {
928            int typeCode = MouseBehaviorCallback.ROTATE;
929            if (type.equals(TransformChangeEvent.Type.TRANSLATE))
930                typeCode = MouseBehaviorCallback.TRANSLATE;
931            else if (type.equals(TransformChangeEvent.Type.ZOOM))
932                typeCode = MouseBehaviorCallback.ZOOM;
933
934            _callback.transformChanged(typeCode, transform);
935        }
936
937        //  Now notify listeners.
938        TransformChangeEvent event = new TransformChangeEvent(this, type, transform);
939        for (TransformChangeListener listener : _transformListeners) {
940            listener.transformChanged(event);
941        }
942    }
943
944    /**
945     * Users can overload this method which is called every time the Behavior updates the
946     * transform.
947     *
948     * This default implementation does nothing.
949     *
950     * @param transform The new 3D transform.
951     */
952    public void transformChanged(Transform3D transform) {
953    }
954
955    /**
956     * The transformChanged method in the callback class will be called every time the
957     * transform is updated
958     *
959     * @param callback The mouse behavior callback. May pass null for no callback.
960     */
961    public void setupCallback(MouseBehaviorCallback callback) {
962        _callback = callback;
963    }
964
965    /**
966     * Calculate a rotation matrix based on the axis and angle of rotation from the last 2
967     * locations of the mouse relative to the VirtualSphere cue circle.
968     *
969     * @param pnt1      The 1st mouse location in the window.
970     * @param pnt2      The 2nd mouse location in the window.
971     * @param cueCenter The center of the virtual sphere in the window.
972     * @param cueRadius The radius of the virtual sphere.
973     * @param rotMatrix Preallocated rotation matrix to be filled in by this method. This
974     *                  matrix will be overwritten by this method.
975     * @return A reference to the input rotMatrix is returned with the elements filled in.
976     */
977    private Transform3D makeRotationMtx(Point pnt1, Point pnt2, Point cueCenter, int cueRadius,
978            Transform3D rotMatrix) {
979
980        // Vectors "op" and "oq" are defined as class variables to avoid wastefull memory allocations.
981        // Project mouse points to 3-D points on the +z hemisphere of a unit sphere.
982        pointOnUnitSphere(pnt1, cueCenter, cueRadius, _op);
983        pointOnUnitSphere(pnt2, cueCenter, cueRadius, _oq);
984
985        /* Consider the two projected points as vectors from the center of the 
986         *  unit sphere. Compute the rotation matrix that will transform vector
987         *  op to oq.  */
988        setRotationMatrix(rotMatrix, _op, _oq);
989
990        return rotMatrix;
991    }
992
993    /**
994     * Project a 2D point on a circle to a 3D point on the +z hemisphere of a unit sphere.
995     * If the 2D point is outside the circle, it is first mapped to the nearest point on
996     * the circle before projection. Orthographic projection is used, though technically
997     * the field of view of the camera should be taken into account. However, the
998     * discrepancy is negligible.
999     *
1000     * @param p         Window point to be projected onto the sphere.
1001     * @param cueCenter Location of center of virtual sphere in window.
1002     * @param cueRadius The radius of the virtual sphere.
1003     * @param v         Storage for the 3D projected point created by this method.
1004     */
1005    private static void pointOnUnitSphere(Point p, Point cueCenter, int cueRadius, Vector3d v) {
1006
1007        /* Turn the mouse points into vectors relative to the center of the circle
1008         *  and normalize them.  Note we need to flip the y value since the 3D coordinate
1009         *  has positive y going up.  */
1010        double vx = (p.x - cueCenter.x) / (double)cueRadius;
1011        double vy = (cueCenter.y - p.y) / (double)cueRadius;
1012        double lengthSqared = vx * vx + vy * vy;
1013
1014        /* Project the point onto the sphere, assuming orthographic projection.
1015         *  Points beyond the virtual sphere are normalized onto 
1016         *  edge of the sphere (where z = 0).  */
1017        double vz = 0;
1018        if (lengthSqared < 1)
1019            vz = sqrt(1.0 - lengthSqared);
1020
1021        else {
1022            double length = sqrt(lengthSqared);
1023            vx /= length;
1024            vy /= length;
1025        }
1026
1027        v.x = vx;
1028        v.y = vy;
1029        v.z = vz;
1030    }
1031
1032    /**
1033     * Computes a`ap (rotate) vectors op onto oq. The rotation is about an axis
1034     * perpendicular to op and oq. Note this routine won't work if op or oq are zero
1035     * vectors, or if they are parallel or antiparallel to each other.
1036     *
1037     * <p>
1038     * Modification of Michael Pique's formula in Graphics Gems Vol. 1. Andrew Glassner,
1039     * Ed. Addison-Wesley.</p>
1040     *
1041     * @param rotationMatrix The rotation matrix to be filled in.
1042     * @param op             The 1st 3D vector.
1043     * @param oq             The 2nd 3D vector.
1044     */
1045    private void setRotationMatrix(Transform3D rotationMatrix, Vector3d op, Vector3d oq) {
1046
1047        // Vector "av" is defined as a class variable to avoid wastefull memory allocations.
1048        _av.cross(op, oq);          //  av = op X oq
1049        double cosA = op.dot(oq);   //  cosA = op * oq
1050
1051        //  Set the axis angle object.
1052        _a1.set(_av, acos(cosA));
1053
1054        //  Set the rotation matrix.
1055        rotationMatrix.set(_a1);
1056    }
1057
1058}