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