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