001 /* 002 * File $Workfile: CanvasPanel.java $ 003 * $Archive: /Tower Of Hanoi/toh/src/net/pacbell/jfai/toh/ui/CanvasPanel.java $ 004 * Last changed by $Author: Jürgen Failenschmid $ 005 * Last modified $Modtime: 12/09/05 4:57p $ 006 * Last checked in $Date: 12/09/05 5:10p $ 007 * $Revision: 8 $ 008 * 009 * 010 * Copyright (C) 2004-2005 Jürgen Failenschmid. All rights reserved. 011 */ 012 013 package net.pacbell.jfai.toh.ui; 014 015 import java.awt.BorderLayout; 016 import java.awt.Dimension; 017 import java.util.List; 018 import java.util.Vector; 019 020 import javax.media.j3d.AmbientLight; 021 import javax.media.j3d.Background; 022 import javax.media.j3d.BoundingSphere; 023 import javax.media.j3d.BranchGroup; 024 import javax.media.j3d.DirectionalLight; 025 import javax.media.j3d.Group; 026 import javax.media.j3d.Light; 027 import javax.media.j3d.PointLight; 028 import javax.media.j3d.Transform3D; 029 import javax.media.j3d.TransformGroup; 030 import javax.swing.BorderFactory; 031 import javax.swing.JPanel; 032 import javax.swing.border.TitledBorder; 033 import javax.vecmath.Color3f; 034 import javax.vecmath.Point3d; 035 import javax.vecmath.Point3f; 036 import javax.vecmath.Vector3d; 037 import javax.vecmath.Vector3f; 038 039 import com.sun.j3d.utils.behaviors.vp.OrbitBehavior; 040 import com.sun.j3d.utils.picking.PickTool; 041 import com.sun.j3d.utils.picking.behaviors.PickingCallback; 042 import com.sun.j3d.utils.universe.SimpleUniverse; 043 044 import net.pacbell.jfai.toh.domain.Base; 045 import net.pacbell.jfai.toh.domain.Disk; 046 import net.pacbell.jfai.toh.domain.Pin; 047 import net.pacbell.jfai.toh.util.BufferedValue; 048 049 import org.apache.commons.logging.Log; 050 import org.apache.commons.logging.LogFactory; 051 052 /** 053 * A panel containing the 3D-view of the puzzle. 054 * 055 * @author {@link <a href="http://www.anycpu.com">Jürgen Failenschmid</a>} 056 */ 057 @SuppressWarnings("serial") 058 public class CanvasPanel extends JPanel 059 { 060 061 /** 062 * Adjusts the viewing distance during the first paint call. The scene's 063 * lowest point is assumed at y = 0, and the scene extends by d equally 064 * around the origin along the z-axis. By default, the view platform is at 065 * (0,0,0). The view platform needs moving away so that the scene can be 066 * viewed in full. The wider the canvas, the closer the view platform can 067 * be to still see the full width of the scene. The optimal viewing 068 * distance depends on the canvas size, which is not known until the canvas 069 * is painted the first time. If the view platform is located at x = y = 0 070 * then to be able to see the virtual width (2 * w) within the current 071 * canvas width - the same distance to either side of the view platform, 072 * the view platform has to be moved on the z-axis by: zw = w / tan(FOV/2), 073 * where FOV is the field of view in radians. With the default view 074 * settings, only the canvas width is considered and the ratio between 075 * window width and height needs to be applied to the scene's height h: zhy = 076 * h * cw / ch / tan(FOV/2) If the view platform is moved along the y-axis 077 * by y measured from the lowest point of the scene then zy = [d / 2 + y / 078 * tan(FOV/2)] * cw / ch. The optimal location of the view platform is 079 * max(zw, zhy, zy). 080 */ 081 private class FirstPaintListener implements Canvas3D.IPaintListener { 082 private static final double SCENE_EXTRA_Z = 0.3d; 083 084 private static final double SCENE_Y_TRANSLATION_FACTOR = 1d; 085 086 /** 087 * The canvas is painted the very first time. Changes the view point so 088 * that the scene is completely visible. 089 * 090 * @see net.pacbell.jfai.toh.ui.Canvas3D.IPaintListener#firstPaintNotify(net.pacbell.jfai.toh.ui.Canvas3D) 091 */ 092 public void firstPaintNotify(Canvas3D aCanvas) { 093 final double tanFov = Math 094 .tan(aCanvas.getView().getFieldOfView() / 2); 095 final double sceneWidth = TowerOfHanoiView.width(); 096 final double sceneHeight = TowerOfHanoiView.height(); 097 final double sceneDepth = TowerOfHanoiView.depth(); 098 final double zw = sceneWidth / 2 / tanFov; 099 100 final Dimension canvasSize = aCanvas.getSize(); 101 final double goodY = sceneHeight * SCENE_Y_TRANSLATION_FACTOR; 102 final double zy = Math.max(sceneDepth / 2 + goodY / tanFov, 103 sceneHeight / tanFov) 104 * canvasSize.width / canvasSize.height; 105 final double goodZ = Math.max(zw, zy) + SCENE_EXTRA_Z; 106 107 final TransformGroup viewingPlatformTransformGroup = getUniverse() 108 .getViewingPlatform().getViewPlatformTransform(); 109 final Transform3D transform = new Transform3D(); 110 viewingPlatformTransformGroup.getTransform(transform); 111 final Vector3d translation = new Vector3d(); 112 transform.get(translation); 113 translation.set(translation.x, goodY, goodZ); 114 // Only set the translation component of the transform 115 transform.setTranslation(translation); 116 viewingPlatformTransformGroup.setTransform(transform); 117 118 if (LOG.isDebugEnabled()) { 119 LOG.debug("firstPaintNotify: tanFOV=" + tanFov //$NON-NLS-1$ 120 + ", scene width=" + sceneWidth //$NON-NLS-1$ 121 + ", scene height=" + sceneHeight //$NON-NLS-1$ 122 + ", canvas size=" + canvasSize //$NON-NLS-1$ 123 + ", zw=" + zw + ", zy=" + zy); //$NON-NLS-1$ //$NON-NLS-2$ 124 LOG.debug(" ==> z-translation = " + translation.z); //$NON-NLS-1$ 125 } 126 } 127 } 128 129 /** 130 * The trace log. 131 */ 132 static final Log LOG = LogFactory.getLog(CanvasPanel.class); 133 134 private static final float AMBIENT_LIGHT__COLOR_SATURATION = 0.3f; 135 private static final Color3f AMBIENT_LIGHT_COLOR = new Color3f( 136 AMBIENT_LIGHT__COLOR_SATURATION, AMBIENT_LIGHT__COLOR_SATURATION, 137 AMBIENT_LIGHT__COLOR_SATURATION); 138 private static final double BOUNDING__SPHERE_RADIUS = 100.0; 139 private static final BoundingSphere BOUNDING_SPHERE = new BoundingSphere( 140 new Point3d(0.0, 0.0, 0.0), BOUNDING__SPHERE_RADIUS); 141 private static final float POINT_LIGHT__ATTENUATION_LINEAR = 0.7f; 142 private static final Point3f POINT_LIGHT_ATTENUATION = new Point3f(0.0f, 143 POINT_LIGHT__ATTENUATION_LINEAR, 0.0f); 144 private BaseView baseView; 145 private Canvas3D canvas; 146 private List<DiskView> diskViews; 147 private MousePicker mousePicker; 148 private TransformGroup sceneTransformGroup; 149 private SimpleUniverse universe; 150 151 /** 152 * Creates a panel with the 3D drawing canvas. 153 * 154 * @param base a Base 155 * @param disks the disks 156 */ 157 public CanvasPanel(Base base, List<Disk> disks) { 158 /* 159 * Need to use a layout manager that anchors the size of the canvas to 160 * the panel's border. Note that the preferred size of a Canvas3D by 161 * default is 0 by 0. 162 */ 163 super(new BorderLayout()); 164 setDiskViews(new Vector<DiskView>()); 165 setBorder(BorderFactory.createTitledBorder(BorderFactory 166 .createLoweredBevelBorder(), 167 "Press mouse buttons and drag to change view", //$NON-NLS-1$ 168 TitledBorder.CENTER, TitledBorder.ABOVE_TOP)); 169 170 setCanvas(new Canvas3D(SimpleUniverse.getPreferredConfiguration())); 171 setUniverse(new SimpleUniverse(getCanvas())); 172 add(getCanvas()); 173 174 // Create a compiled scene graph 175 final BranchGroup scene = createSceneGraph(base, disks); 176 177 /* 178 * Register a listener to adjust the viewing distance during the first 179 * paint call 180 */ 181 getCanvas().addPaintListener(new FirstPaintListener()); 182 183 // Adds mouse orbit behavior to the ViewingPlatform 184 final OrbitBehavior orbit = new OrbitBehavior(getCanvas(), 185 OrbitBehavior.REVERSE_ALL); 186 orbit.setSchedulingBounds(BOUNDING_SPHERE); 187 getUniverse().getViewingPlatform().setViewPlatformBehavior(orbit); 188 // Adds scene graph to the universe 189 getUniverse().addBranchGraph(scene); 190 } 191 192 /** 193 * Prepares for the selection of a pin in the scene. The parameter is the 194 * editor model for the pin that is to be selected. 195 * <p> 196 * <p> 197 * <i>Assume that this method is called in the event dispatch thread, for 198 * example, when a toggle button was pressed. </i> 199 * 200 * @param pinModel a BufferedValue for the selected pin 201 */ 202 public void beginPinSelection(final BufferedValue pinModel) { 203 getMousePicker().setCallback(new PickingCallback() { 204 /** 205 * {@inheritDoc} 206 * <p> 207 * MousePicker calls this for each pick attempt. If type is 208 * PickingCallback.ROTATE, an object was selected and the second 209 * argument is its TransformGroup. 210 * </p> 211 */ 212 public void transformChanged(int type, TransformGroup tg) { 213 if (LOG.isDebugEnabled()) { 214 final StringBuffer logMessage = new StringBuffer(); 215 logMessage 216 .append("PickingCallback: type = " + type + ", transform group = " + tg); //$NON-NLS-1$ //$NON-NLS-2$ 217 if (tg != null) { 218 logMessage 219 .append(tg.getUserData() == null ? " - no user data" //$NON-NLS-1$ 220 : " - user data = " + tg.getUserData()); //$NON-NLS-1$ 221 } 222 else { 223 logMessage.append(" - no pick"); //$NON-NLS-1$ 224 } 225 LOG.debug(logMessage); 226 } 227 if (type == PickingCallback.ROTATE 228 && tg.getUserData() instanceof Pin) 229 { 230 pickPin(pinModel, (Pin) tg.getUserData()); 231 } 232 } 233 }); 234 } 235 236 /** Ends the visualization. */ 237 public void cleanUp() { 238 LOG.debug("clean up"); //$NON-NLS-1$ 239 getUniverse().removeAllLocales(); 240 } 241 242 /** 243 * Ends pin selection. 244 */ 245 public void endPinSelection() { 246 // Remove mouse picker callback 247 getMousePicker().setCallback(null); 248 } 249 250 /** 251 * Gets the 3D canvas. 252 * 253 * @return Returns the 3D canvas. 254 */ 255 public Canvas3D getCanvas() { 256 return canvas; 257 } 258 259 /** 260 * Gets the list of disk views. 261 * 262 * @return a list of the views of disks 263 */ 264 public List<DiskView> getDiskViews() { 265 return diskViews; 266 } 267 268 /** 269 * Refreshes all disk views by removing all existing disk views and 270 * creating a view for each of the given disks. 271 * 272 * @param disks the disks 273 */ 274 public void updateDisks(List<Disk> disks) { 275 removeDisks(); 276 addDisks(disks); 277 } 278 279 /** 280 * A pin was selected in the 3D view. Changes the given buffered value. 281 * <p> 282 * <p> 283 * <i>This could be called outside of the event dispatch thread. </i> 284 * 285 * @param pinModel a BufferedValue for the selected pin 286 * @param pin a Pin 287 */ 288 protected void pickPin(BufferedValue pinModel, Pin pin) { 289 /* Has protected visibility for faster inner class access */ 290 pinModel.setValue(pin); 291 } 292 293 /** 294 * Gets the Universe. 295 * 296 * @return the SimpleUniverse 297 */ 298 SimpleUniverse getUniverse() { 299 return universe; 300 } 301 302 /** 303 * Adds a view for each disk located at the base's source pin. 304 */ 305 private void addDisks(List<Disk> disks) { 306 DiskView.setHeight(PinView.stackHeight() / disks.size()); 307 DiskView diskView; 308 for (Disk disk : disks) { 309 diskView = new DiskView(disk, getBaseView().pinView(disk.getPin()) 310 .diskLocation(disk), disks.size(), getBaseView()); 311 getDiskViews().add(diskView); 312 getSceneTransformGroup().addChild(diskView.getGroup()); 313 } 314 } 315 316 private Background createBackground() { 317 final Background background = new Background(new Color3f(0.0f, 0.0f, 318 0.0f)); 319 background.setApplicationBounds(BOUNDING_SPHERE); 320 return background; 321 } 322 323 private void createLights(TransformGroup sceneTransform) { 324 // Adds ambient light 325 final AmbientLight ambientLight = new AmbientLight( 326 CanvasPanel.AMBIENT_LIGHT_COLOR); 327 ambientLight.setInfluencingBounds(BOUNDING_SPHERE); 328 sceneTransform.addChild(ambientLight); 329 330 /* 331 * Adds a white directional light positioned in the foreground on the 332 * left 333 */ 334 final Color3f lightColor = new Color3f(1.0f, 1.0f, 1.0f); 335 final Transform3D light1Transform = new Transform3D(); 336 final Vector3d light1Position = new Vector3d(-1.0, 0.0, 2.0); 337 light1Transform.set(light1Position); 338 sceneTransform.addChild(new TransformGroup(light1Transform)); 339 Light directionalLight1; 340 final Vector3f light1Direction = new Vector3f(light1Position); 341 light1Direction.negate(); 342 directionalLight1 = new DirectionalLight(lightColor, light1Direction); 343 directionalLight1.setInfluencingBounds(BOUNDING_SPHERE); 344 sceneTransform.addChild(directionalLight1); 345 346 /* 347 * Adds a white point light positioned above in the back on the right 348 * side 349 */ 350 final Transform3D light2Transform = new Transform3D(); 351 final Point3f light2Position = new Point3f(1.0f, 2.0f, -1.0f); 352 light2Transform.set(new Vector3f(light2Position)); 353 sceneTransform.addChild(new TransformGroup(light2Transform)); 354 final Light pointLight1 = new PointLight(lightColor, light2Position, 355 CanvasPanel.POINT_LIGHT_ATTENUATION); 356 pointLight1.setInfluencingBounds(BOUNDING_SPHERE); 357 sceneTransform.addChild(pointLight1); 358 } 359 360 private BranchGroup createSceneGraph(Base base, List<Disk> disks) { 361 // Creates the root of the branch graph 362 final BranchGroup root = new BranchGroup(); 363 364 /* 365 * Creates a TransformGroup for all objects and adds it to the scene. 366 * This transform is currently the identity transform. 367 */ 368 final Transform3D sceneOrientation = new Transform3D(); 369 final TransformGroup sceneTransform = new TransformGroup( 370 sceneOrientation); 371 setSceneTransformGroup(sceneTransform); 372 root.addChild(sceneTransform); 373 sceneTransform.setCapability(Group.ALLOW_CHILDREN_WRITE); 374 sceneTransform.setCapability(Group.ALLOW_CHILDREN_EXTEND); 375 376 // Adds the background 377 sceneTransform.addChild(createBackground()); 378 379 // Adds lights 380 createLights(sceneTransform); 381 382 // Creates the base view and adds it to the scene 383 setBaseView(new BaseView(base)); 384 sceneTransform.addChild(getBaseView().getGroup()); 385 386 // Adds the disks to the scene 387 addDisks(disks); 388 389 // Adds mouse pick behavior 390 setMousePicker(new MousePicker(root, getCanvas(), BOUNDING_SPHERE, 391 PickTool.GEOMETRY)); 392 root.addChild(getMousePicker()); 393 394 root.compile(); 395 return root; 396 } 397 398 /** 399 * Gets the view of the base. 400 * 401 * @return the view of the base 402 */ 403 private BaseView getBaseView() { 404 return baseView; 405 } 406 407 /** 408 * Gets the mouse picker. 409 * 410 * @return a MousePicker 411 */ 412 private MousePicker getMousePicker() { 413 return mousePicker; 414 } 415 416 /** 417 * Gets the TransformGroup. 418 * 419 * @return the TransformGroup 420 */ 421 private TransformGroup getSceneTransformGroup() { 422 return sceneTransformGroup; 423 } 424 425 /** 426 * Removes all DiskViews from the scene. 427 */ 428 private void removeDisks() { 429 for (DiskView diskView : getDiskViews()) { 430 diskView.disappear(); 431 } 432 433 getDiskViews().clear(); 434 } 435 436 private void setBaseView(BaseView baseView) { 437 this.baseView = baseView; 438 } 439 440 /** 441 * Sets the canvas. 442 * 443 * @param canvas The canvas to set. 444 */ 445 private void setCanvas(Canvas3D canvas) { 446 this.canvas = canvas; 447 } 448 449 private void setDiskViews(List<DiskView> newDiskViews) { 450 diskViews = newDiskViews; 451 } 452 453 private void setMousePicker(MousePicker newMousePicker) { 454 mousePicker = newMousePicker; 455 } 456 457 private void setSceneTransformGroup(TransformGroup newSceneTransformGroup) { 458 sceneTransformGroup = newSceneTransformGroup; 459 } 460 461 private void setUniverse(SimpleUniverse universe) { 462 this.universe = universe; 463 } 464 }