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