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