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}