1 /* 2 * HomeComponent3D.java 24 ao?t 2006 3 * 4 * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 package com.eteks.sweethome3d.swing; 21 22 import java.awt.AlphaComposite; 23 import java.awt.Color; 24 import java.awt.Component; 25 import java.awt.ComponentOrientation; 26 import java.awt.Composite; 27 import java.awt.Container; 28 import java.awt.Dimension; 29 import java.awt.EventQueue; 30 import java.awt.Graphics; 31 import java.awt.Graphics2D; 32 import java.awt.GraphicsConfiguration; 33 import java.awt.GraphicsEnvironment; 34 import java.awt.GridBagConstraints; 35 import java.awt.GridBagLayout; 36 import java.awt.GridLayout; 37 import java.awt.Insets; 38 import java.awt.LayoutManager; 39 import java.awt.Point; 40 import java.awt.Rectangle; 41 import java.awt.RenderingHints; 42 import java.awt.event.ActionEvent; 43 import java.awt.event.ActionListener; 44 import java.awt.event.ComponentAdapter; 45 import java.awt.event.ComponentEvent; 46 import java.awt.event.ComponentListener; 47 import java.awt.event.MouseAdapter; 48 import java.awt.event.MouseEvent; 49 import java.awt.event.MouseListener; 50 import java.awt.event.MouseMotionAdapter; 51 import java.awt.event.MouseMotionListener; 52 import java.awt.event.MouseWheelEvent; 53 import java.awt.event.MouseWheelListener; 54 import java.awt.geom.Area; 55 import java.awt.geom.GeneralPath; 56 import java.awt.geom.PathIterator; 57 import java.awt.geom.Rectangle2D; 58 import java.awt.image.BufferedImage; 59 import java.awt.image.FilteredImageSource; 60 import java.awt.image.RGBImageFilter; 61 import java.awt.print.PageFormat; 62 import java.awt.print.Printable; 63 import java.beans.PropertyChangeEvent; 64 import java.beans.PropertyChangeListener; 65 import java.lang.ref.WeakReference; 66 import java.lang.reflect.Field; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Collection; 70 import java.util.Collections; 71 import java.util.Comparator; 72 import java.util.Enumeration; 73 import java.util.HashMap; 74 import java.util.HashSet; 75 import java.util.List; 76 import java.util.Map; 77 import java.util.Set; 78 import java.util.TreeMap; 79 import java.util.concurrent.Executors; 80 import java.util.concurrent.ScheduledExecutorService; 81 import java.util.concurrent.TimeUnit; 82 83 import javax.media.j3d.Alpha; 84 import javax.media.j3d.AmbientLight; 85 import javax.media.j3d.Appearance; 86 import javax.media.j3d.Background; 87 import javax.media.j3d.BoundingBox; 88 import javax.media.j3d.BoundingSphere; 89 import javax.media.j3d.Bounds; 90 import javax.media.j3d.BranchGroup; 91 import javax.media.j3d.Canvas3D; 92 import javax.media.j3d.ColoringAttributes; 93 import javax.media.j3d.DirectionalLight; 94 import javax.media.j3d.Geometry; 95 import javax.media.j3d.GraphicsConfigTemplate3D; 96 import javax.media.j3d.Group; 97 import javax.media.j3d.IllegalRenderingStateException; 98 import javax.media.j3d.J3DGraphics2D; 99 import javax.media.j3d.Light; 100 import javax.media.j3d.Link; 101 import javax.media.j3d.Material; 102 import javax.media.j3d.Node; 103 import javax.media.j3d.PointArray; 104 import javax.media.j3d.RenderingAttributes; 105 import javax.media.j3d.Shape3D; 106 import javax.media.j3d.TexCoordGeneration; 107 import javax.media.j3d.Texture; 108 import javax.media.j3d.TextureAttributes; 109 import javax.media.j3d.Transform3D; 110 import javax.media.j3d.TransformGroup; 111 import javax.media.j3d.TransformInterpolator; 112 import javax.media.j3d.TransparencyAttributes; 113 import javax.media.j3d.View; 114 import javax.media.j3d.VirtualUniverse; 115 import javax.swing.AbstractAction; 116 import javax.swing.ActionMap; 117 import javax.swing.ImageIcon; 118 import javax.swing.InputMap; 119 import javax.swing.JButton; 120 import javax.swing.JComponent; 121 import javax.swing.JPanel; 122 import javax.swing.JPopupMenu; 123 import javax.swing.KeyStroke; 124 import javax.swing.RepaintManager; 125 import javax.swing.SwingUtilities; 126 import javax.swing.Timer; 127 import javax.swing.border.Border; 128 import javax.swing.event.AncestorEvent; 129 import javax.swing.event.AncestorListener; 130 import javax.swing.event.ChangeEvent; 131 import javax.swing.event.ChangeListener; 132 import javax.swing.event.MouseInputAdapter; 133 import javax.vecmath.Color3f; 134 import javax.vecmath.Point3d; 135 import javax.vecmath.Point3f; 136 import javax.vecmath.TexCoord2f; 137 import javax.vecmath.Vector3f; 138 import javax.vecmath.Vector4f; 139 140 import com.eteks.sweethome3d.j3d.Component3DManager; 141 import com.eteks.sweethome3d.j3d.Ground3D; 142 import com.eteks.sweethome3d.j3d.HomePieceOfFurniture3D; 143 import com.eteks.sweethome3d.j3d.ModelManager; 144 import com.eteks.sweethome3d.j3d.Object3DBranch; 145 import com.eteks.sweethome3d.j3d.Object3DBranchFactory; 146 import com.eteks.sweethome3d.j3d.TextureManager; 147 import com.eteks.sweethome3d.j3d.Wall3D; 148 import com.eteks.sweethome3d.model.Camera; 149 import com.eteks.sweethome3d.model.CollectionEvent; 150 import com.eteks.sweethome3d.model.CollectionListener; 151 import com.eteks.sweethome3d.model.Elevatable; 152 import com.eteks.sweethome3d.model.Home; 153 import com.eteks.sweethome3d.model.HomeDoorOrWindow; 154 import com.eteks.sweethome3d.model.HomeEnvironment; 155 import com.eteks.sweethome3d.model.HomeFurnitureGroup; 156 import com.eteks.sweethome3d.model.HomeLight; 157 import com.eteks.sweethome3d.model.HomePieceOfFurniture; 158 import com.eteks.sweethome3d.model.HomeTexture; 159 import com.eteks.sweethome3d.model.Label; 160 import com.eteks.sweethome3d.model.Level; 161 import com.eteks.sweethome3d.model.Polyline; 162 import com.eteks.sweethome3d.model.Room; 163 import com.eteks.sweethome3d.model.Selectable; 164 import com.eteks.sweethome3d.model.UserPreferences; 165 import com.eteks.sweethome3d.model.Wall; 166 import com.eteks.sweethome3d.tools.OperatingSystem; 167 import com.eteks.sweethome3d.viewcontroller.HomeController3D; 168 import com.eteks.sweethome3d.viewcontroller.Object3DFactory; 169 import com.sun.j3d.exp.swing.JCanvas3D; 170 import com.sun.j3d.utils.geometry.GeometryInfo; 171 import com.sun.j3d.utils.picking.PickCanvas; 172 import com.sun.j3d.utils.picking.PickResult; 173 import com.sun.j3d.utils.universe.SimpleUniverse; 174 import com.sun.j3d.utils.universe.Viewer; 175 import com.sun.j3d.utils.universe.ViewingPlatform; 176 177 /** 178 * A component that displays home walls, rooms and furniture with Java 3D. 179 * @author Emmanuel Puybaret 180 */ 181 public class HomeComponent3D extends JComponent implements com.eteks.sweethome3d.viewcontroller.View, Printable { 182 private enum ActionType {MOVE_CAMERA_FORWARD, MOVE_CAMERA_FAST_FORWARD, MOVE_CAMERA_BACKWARD, MOVE_CAMERA_FAST_BACKWARD, 183 MOVE_CAMERA_LEFT, MOVE_CAMERA_FAST_LEFT, MOVE_CAMERA_RIGHT, MOVE_CAMERA_FAST_RIGHT, 184 ROTATE_CAMERA_YAW_LEFT, ROTATE_CAMERA_YAW_FAST_LEFT, ROTATE_CAMERA_YAW_RIGHT, ROTATE_CAMERA_YAW_FAST_RIGHT, 185 ROTATE_CAMERA_PITCH_UP, ROTATE_CAMERA_PITCH_FAST_UP, ROTATE_CAMERA_PITCH_DOWN, ROTATE_CAMERA_PITCH_FAST_DOWN, 186 ELEVATE_CAMERA_UP, ELEVATE_CAMERA_FAST_UP, ELEVATE_CAMERA_DOWN, ELEVATE_CAMERA_FAST_DOWN} 187 188 private static final boolean JAVA3D_1_5 = VirtualUniverse.getProperties().get("j3d.version") != null 189 && ((String)VirtualUniverse.getProperties().get("j3d.version")).startsWith("1.5"); 190 191 private final Home home; 192 private final boolean displayShadowOnFloor; 193 private final Object3DFactory object3dFactory; 194 private final Map<Selectable, Object3DBranch> homeObjects = new HashMap<Selectable, Object3DBranch>(); 195 private Light [] sceneLights; 196 private Collection<Selectable> homeObjectsToUpdate; 197 private Collection<Selectable> lightScopeObjectsToUpdate; 198 private Component component3D; 199 private SimpleUniverse onscreenUniverse; 200 private Camera camera; 201 // Listeners bound to home that updates 3D scene objects 202 private PropertyChangeListener cameraChangeListener; 203 private PropertyChangeListener homeCameraListener; 204 private PropertyChangeListener backgroundChangeListener; 205 private PropertyChangeListener groundChangeListener; 206 private PropertyChangeListener backgroundLightColorListener; 207 private PropertyChangeListener lightColorListener; 208 private PropertyChangeListener subpartSizeListener; 209 private PropertyChangeListener elevationChangeListener; 210 private PropertyChangeListener wallsAlphaListener; 211 private PropertyChangeListener drawingModeListener; 212 private CollectionListener<Level> levelListener; 213 private PropertyChangeListener levelChangeListener; 214 private CollectionListener<Wall> wallListener; 215 private PropertyChangeListener wallChangeListener; 216 private CollectionListener<HomePieceOfFurniture> furnitureListener; 217 private PropertyChangeListener furnitureChangeListener; 218 private CollectionListener<Room> roomListener; 219 private PropertyChangeListener roomChangeListener; 220 private CollectionListener<Polyline> polylineListener; 221 private PropertyChangeListener polylineChangeListener; 222 private CollectionListener<Label> labelListener; 223 private PropertyChangeListener labelChangeListener; 224 // Offscreen printed image cache 225 // Creating an offscreen buffer is a quite lengthy operation so we keep the last printed image in this field 226 // This image should be set to null each time the 3D view changes 227 private BufferedImage printedImageCache; 228 private BoundingBox approximateHomeBoundsCache; 229 private SimpleUniverse offscreenUniverse; 230 231 private JComponent navigationPanel; 232 private ComponentListener navigationPanelListener; 233 private BufferedImage navigationPanelImage; 234 private Area lightScopeOutsideWallsAreaCache; 235 236 /** 237 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture, 238 * with no controller. 239 * @throws IllegalStateException if the 3D component couldn't be created. 240 */ HomeComponent3D(Home home)241 public HomeComponent3D(Home home) { 242 this(home, null); 243 } 244 245 /** 246 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture. 247 * @throws IllegalStateException if the 3D component couldn't be created. 248 */ HomeComponent3D(Home home, HomeController3D controller)249 public HomeComponent3D(Home home, HomeController3D controller) { 250 this(home, null, controller); 251 } 252 253 /** 254 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture, 255 * with shadows on the floor. 256 * @throws IllegalStateException if the 3D component couldn't be created. 257 */ HomeComponent3D(Home home, UserPreferences preferences, boolean displayShadowOnFloor)258 public HomeComponent3D(Home home, 259 UserPreferences preferences, 260 boolean displayShadowOnFloor) { 261 this(home, preferences, new Object3DBranchFactory(preferences), displayShadowOnFloor, null); 262 } 263 264 /** 265 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture. 266 * @throws IllegalStateException if the 3D component couldn't be created. 267 */ HomeComponent3D(Home home, UserPreferences preferences, HomeController3D controller)268 public HomeComponent3D(Home home, 269 UserPreferences preferences, 270 HomeController3D controller) { 271 this(home, preferences, new Object3DBranchFactory(preferences), false, controller); 272 } 273 274 /** 275 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture. 276 * @param home the home to display in this component 277 * @param preferences user preferences 278 * @param object3dFactory a factory able to create 3D objects from <code>home</code> items. 279 * The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of 280 * this factory is expected to return an instance of {@link Object3DBranch} in current implementation. 281 * @param controller the controller that manages modifications in <code>home</code>. 282 * @throws IllegalStateException if the 3D component couldn't be created. 283 */ HomeComponent3D(Home home, UserPreferences preferences, Object3DFactory object3dFactory, HomeController3D controller)284 public HomeComponent3D(Home home, 285 UserPreferences preferences, 286 Object3DFactory object3dFactory, 287 HomeController3D controller) { 288 this(home, preferences, object3dFactory, false, controller); 289 } 290 291 /** 292 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture. 293 * @throws IllegalStateException if the 3D component couldn't be created. 294 */ HomeComponent3D(Home home, UserPreferences preferences, Object3DFactory object3dFactory, boolean displayShadowOnFloor, HomeController3D controller)295 public HomeComponent3D(Home home, 296 UserPreferences preferences, 297 Object3DFactory object3dFactory, 298 boolean displayShadowOnFloor, 299 HomeController3D controller) { 300 this.home = home; 301 this.displayShadowOnFloor = displayShadowOnFloor; 302 this.object3dFactory = object3dFactory != null 303 ? object3dFactory 304 : new Object3DBranchFactory(preferences); 305 306 if (controller != null) { 307 createActions(controller); 308 installKeyboardActions(); 309 // Let this component manage focus 310 setFocusable(true); 311 SwingTools.installFocusBorder(this); 312 } 313 314 GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment(); 315 if (graphicsEnvironment.getScreenDevices().length == 1) { 316 // If only one screen device is available, create canvas 3D immediately, 317 // otherwise create it once the screen device of the parent is known 318 createComponent3D(graphicsEnvironment.getDefaultScreenDevice().getDefaultConfiguration(), preferences, controller); 319 } 320 321 // Add an ancestor listener to create canvas 3D and its universe once this component is made visible 322 // and clean up universe once its parent frame is disposed 323 addAncestorListener(preferences, controller, displayShadowOnFloor); 324 } 325 326 /** 327 * Adds an ancestor listener to this component to manage the creation of the canvas and its universe 328 * and clean up the universe. 329 */ addAncestorListener(final UserPreferences preferences, final HomeController3D controller, final boolean displayShadowOnFloor)330 private void addAncestorListener(final UserPreferences preferences, 331 final HomeController3D controller, 332 final boolean displayShadowOnFloor) { 333 addAncestorListener(new AncestorListener() { 334 public void ancestorAdded(AncestorEvent ev) { 335 if (offscreenUniverse != null) { 336 throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time"); 337 } 338 339 // Create component 3D only once it's visible 340 Insets insets = getInsets(); 341 if (getHeight() <= insets.top + insets.bottom 342 || getWidth() <= insets.left + insets.right) { 343 addComponentListener(new ComponentAdapter() { 344 @Override 345 public void componentResized(ComponentEvent ev) { 346 removeComponentListener(this); 347 // If 3D view is still in component hierarchy, create component children 348 if (SwingUtilities.getRoot(HomeComponent3D.this) != null) { 349 ancestorAdded(null); 350 } 351 } 352 }); 353 return; 354 } else if (ev == null) { 355 // Force a resize event to make the component 3D appear 356 Component root = SwingUtilities.getRoot(HomeComponent3D.this); 357 root.dispatchEvent(new ComponentEvent(root, ComponentEvent.COMPONENT_RESIZED)); 358 } 359 360 // Create component 3D only once the graphics configuration of its parent is known 361 if (component3D == null) { 362 createComponent3D(getGraphicsConfiguration(), preferences, controller); 363 } 364 if (onscreenUniverse == null) { 365 onscreenUniverse = createUniverse(displayShadowOnFloor, true, false); 366 Canvas3D canvas3D; 367 if (component3D instanceof Canvas3D) { 368 canvas3D = (Canvas3D)component3D; 369 } else { 370 try { 371 // Call JCanvas3D#getOffscreenCanvas3D by reflection to be able to run under Java 3D 1.3 372 canvas3D = (Canvas3D)Class.forName("com.sun.j3d.exp.swing.JCanvas3D").getMethod("getOffscreenCanvas3D").invoke(component3D); 373 } catch (Exception ex) { 374 UnsupportedOperationException ex2 = new UnsupportedOperationException(); 375 ex2.initCause(ex); 376 throw ex2; 377 } 378 } 379 // Bind universe to canvas3D 380 onscreenUniverse.getViewer().getView().addCanvas3D(canvas3D); 381 component3D.setFocusable(false); 382 updateNavigationPanelImage(); 383 } 384 } 385 386 public void ancestorRemoved(AncestorEvent ev) { 387 if (onscreenUniverse != null) { 388 onscreenUniverse.cleanup(); 389 removeHomeListeners(); 390 onscreenUniverse = null; 391 } 392 if (component3D != null) { 393 removeAll(); 394 for (MouseListener l : component3D.getMouseListeners()) { 395 component3D.removeMouseListener(l); 396 } 397 for (MouseMotionListener l : component3D.getMouseMotionListeners()) { 398 component3D.removeMouseMotionListener(l); 399 } 400 for (MouseWheelListener l : component3D.getMouseWheelListeners()) { 401 component3D.removeMouseWheelListener(l); 402 } 403 component3D = null; 404 navigationPanel = null; 405 } 406 } 407 408 public void ancestorMoved(AncestorEvent ev) { 409 } 410 }); 411 } 412 413 /** 414 * Creates the 3D component associated with the given <code>configuration</code> device. 415 */ createComponent3D(GraphicsConfiguration configuration, UserPreferences preferences, HomeController3D controller)416 private void createComponent3D(GraphicsConfiguration configuration, 417 UserPreferences preferences, 418 HomeController3D controller) { 419 if (Boolean.getBoolean("com.eteks.sweethome3d.j3d.useOffScreen3DView")) { 420 GraphicsConfigTemplate3D template = new GraphicsConfigTemplate3D(); 421 template.setSceneAntialiasing(GraphicsConfigTemplate3D.PREFERRED); 422 // Request depth size equal to 24 if supported 423 int defaultDepthSize = template.getDepthSize(); 424 template.setDepthSize(24); 425 if (!template.isGraphicsConfigSupported( 426 GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration())) { 427 template.setDepthSize(defaultDepthSize); 428 } 429 try { 430 // Instantiate JCanvas3DWithNavigationPanel inner class by reflection 431 // to be able to run under Java 3D 1.3 432 this.component3D = (Component)Class.forName(getClass().getName() + "$JCanvas3DWithNavigationPanel"). 433 getConstructor(getClass(), GraphicsConfigTemplate3D.class).newInstance(this, template); 434 this.component3D.setSize(1, 1); 435 } catch (ClassNotFoundException ex) { 436 throw new UnsupportedOperationException("Java 3D 1.5 required to display an offscreen 3D view"); 437 } catch (Exception ex) { 438 UnsupportedOperationException ex2 = new UnsupportedOperationException(); 439 ex2.initCause(ex); 440 throw ex2; 441 } 442 } else { 443 this.component3D = Component3DManager.getInstance().getOnscreenCanvas3D(configuration, 444 new Component3DManager.RenderingObserver() { 445 private Shape3D dummyShape; 446 447 public void canvas3DSwapped(Canvas3D canvas3D) { 448 } 449 450 public void canvas3DPreRendered(Canvas3D canvas3D) { 451 } 452 453 public void canvas3DPostRendered(Canvas3D canvas3D) { 454 // Copy reference to navigation panel image to avoid concurrency problems 455 // if it's modified in the EDT while this method draws it 456 BufferedImage navigationPanelImage = HomeComponent3D.this.navigationPanelImage; 457 // Render navigation panel upon canvas 3D if it exists 458 if (navigationPanelImage != null) { 459 if (JAVA3D_1_5) { 460 // Render trivial transparent shape to reset the possible transformation set on the last rendered texture 461 // See https://jogamp.org/bugzilla/show_bug.cgi?id=1006#c1 462 if (this.dummyShape == null) { 463 PointArray dummyGeometry = new PointArray(1, PointArray.COORDINATES); 464 dummyGeometry.setCoordinates(0, new float [] {0, 0, 0}); 465 Appearance appearance = new Appearance(); 466 appearance.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.FASTEST, 1)); 467 this.dummyShape = new Shape3D(dummyGeometry, appearance); 468 } 469 canvas3D.getGraphicsContext3D().draw(this.dummyShape); 470 } 471 J3DGraphics2D g2D = canvas3D.getGraphics2D(); 472 g2D.drawImage(navigationPanelImage, null, 0, 0); 473 g2D.flush(true); 474 } 475 } 476 }); 477 } 478 this.component3D.setBackground(new Color(230, 230, 230)); 479 480 JPanel canvasPanel = new JPanel(new LayoutManager() { 481 public void addLayoutComponent(String name, Component comp) { 482 } 483 484 public void removeLayoutComponent(Component comp) { 485 } 486 487 public Dimension preferredLayoutSize(Container parent) { 488 return component3D.getPreferredSize(); 489 } 490 491 public Dimension minimumLayoutSize(Container parent) { 492 return component3D.getMinimumSize(); 493 } 494 495 public void layoutContainer(Container parent) { 496 component3D.setBounds(0, 0, Math.max(1, parent.getWidth()), Math.max(1, parent.getHeight())); 497 if (navigationPanel != null 498 && navigationPanel.isVisible()) { 499 // Ensure that navigationPanel is always in top corner 500 Dimension preferredSize = navigationPanel.getPreferredSize(); 501 navigationPanel.setBounds(0, 0, preferredSize.width, preferredSize.height); 502 } 503 } 504 }); 505 506 canvasPanel.add(this.component3D); 507 setLayout(new GridLayout()); 508 add(canvasPanel); 509 if (controller != null) { 510 addMouseListeners(controller, this.component3D); 511 // Add mouse listeners again to ensure 3D component will receive events 512 for (MouseListener l : getMouseListeners()) { 513 super.removeMouseListener(l); 514 addMouseListener(l); 515 } 516 for (MouseMotionListener l : getMouseMotionListeners()) { 517 super.removeMouseMotionListener(l); 518 addMouseMotionListener(l); 519 } 520 if (preferences != null 521 && (!OperatingSystem.isMacOSX() 522 || OperatingSystem.isMacOSXLeopardOrSuperior())) { 523 // No support for navigation panel under Mac OS X Tiger 524 // (too unstable, may crash system at 3D view resizing) 525 this.navigationPanel = createNavigationPanel(this.home, preferences, controller); 526 setNavigationPanelVisible(preferences.isNavigationPanelVisible() && isVisible()); 527 preferences.addPropertyChangeListener(UserPreferences.Property.NAVIGATION_PANEL_VISIBLE, 528 new NavigationPanelChangeListener(this)); 529 } 530 createActions(controller); 531 installKeyboardActions(); 532 // Let this component manage focus 533 setFocusable(true); 534 SwingTools.installFocusBorder(this); 535 } 536 } 537 538 /** 539 * A <code>JCanvas</code> canvas that displays the navigation panel of a home component 3D upon it. 540 */ 541 private static class JCanvas3DWithNavigationPanel extends JCanvas3D { 542 private final HomeComponent3D homeComponent3D; 543 JCanvas3DWithNavigationPanel(HomeComponent3D homeComponent3D, GraphicsConfigTemplate3D template)544 public JCanvas3DWithNavigationPanel(HomeComponent3D homeComponent3D, 545 GraphicsConfigTemplate3D template) { 546 super(template); 547 this.homeComponent3D = homeComponent3D; 548 } 549 paintComponent(Graphics g)550 public void paintComponent(Graphics g) { 551 super.paintComponent(g); 552 g.drawImage(this.homeComponent3D.navigationPanelImage, 0, 0, this); 553 } 554 } 555 556 @Override setVisible(boolean visible)557 public void setVisible(boolean visible) { 558 super.setVisible(visible); 559 if (this.component3D != null) { 560 this.component3D.setVisible(visible); 561 } 562 } 563 564 /** 565 * Preferences property listener bound to this component with a weak reference to avoid 566 * strong link between preferences and this component. 567 */ 568 private static class NavigationPanelChangeListener implements PropertyChangeListener { 569 private final WeakReference<HomeComponent3D> homeComponent3D; 570 NavigationPanelChangeListener(HomeComponent3D homeComponent3D)571 public NavigationPanelChangeListener(HomeComponent3D homeComponent3D) { 572 this.homeComponent3D = new WeakReference<HomeComponent3D>(homeComponent3D); 573 } 574 propertyChange(PropertyChangeEvent ev)575 public void propertyChange(PropertyChangeEvent ev) { 576 // If home pane was garbage collected, remove this listener from preferences 577 HomeComponent3D homeComponent3D = this.homeComponent3D.get(); 578 if (homeComponent3D == null) { 579 ((UserPreferences)ev.getSource()).removePropertyChangeListener( 580 UserPreferences.Property.NAVIGATION_PANEL_VISIBLE, this); 581 } else { 582 homeComponent3D.setNavigationPanelVisible((Boolean)ev.getNewValue() && homeComponent3D.isVisible()); 583 } 584 } 585 } 586 587 /** 588 * Returns the component displayed as navigation panel by this 3D view. 589 */ createNavigationPanel(Home home, UserPreferences preferences, HomeController3D controller)590 private JComponent createNavigationPanel(Home home, 591 UserPreferences preferences, 592 HomeController3D controller) { 593 JPanel navigationPanel = new JPanel(new GridBagLayout()) { 594 @Override 595 public void applyComponentOrientation(ComponentOrientation o) { 596 // Ignore orientation 597 } 598 }; 599 String navigationPanelIconPath = preferences.getLocalizedString(HomeComponent3D.class, "navigationPanel.icon"); 600 final ImageIcon nagivationPanelIcon = navigationPanelIconPath.length() > 0 601 ? new ImageIcon(HomeComponent3D.class.getResource(navigationPanelIconPath)) 602 : null; 603 navigationPanel.setBorder(new Border() { 604 public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { 605 if (nagivationPanelIcon != null) { 606 nagivationPanelIcon.paintIcon(c, g, x, y); 607 } else { 608 // Draw a surrounding oval if no navigation panel icon is defined 609 Graphics2D g2D = (Graphics2D)g; 610 g2D.setColor(Color.BLACK); 611 g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 612 g2D.drawOval(x + 3, y + 3, width - 6, height - 6); 613 } 614 } 615 616 public Insets getBorderInsets(Component c) { 617 return new Insets(2, 2, 2, 2); 618 } 619 620 public boolean isBorderOpaque() { 621 return false; 622 } 623 }); 624 navigationPanel.setOpaque(false); 625 navigationPanel.add(new NavigationButton(0, -(float)Math.PI / 36, 0, "TURN_LEFT", preferences, controller), 626 new GridBagConstraints(0, 1, 1, 2, 0, 0, 627 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 3, 0, 0), 0, 0)); 628 navigationPanel.add(new NavigationButton(12.5f, 0, 0, "GO_FORWARD", preferences, controller), 629 new GridBagConstraints(1, 0, 1, 1, 0, 0, 630 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(3, 0, 0, 0), 0, 0)); 631 navigationPanel.add(new NavigationButton(0, (float)Math.PI / 36, 0, "TURN_RIGHT", preferences, controller), 632 new GridBagConstraints(2, 1, 1, 2, 0, 0, 633 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 2), 0, 0)); 634 navigationPanel.add(new NavigationButton(-12.5f, 0, 0, "GO_BACKWARD", preferences, controller), 635 new GridBagConstraints(1, 3, 1, 1, 0, 0, 636 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0)); 637 navigationPanel.add(new NavigationButton(0, 0, -(float)Math.PI / 100, "TURN_UP", preferences, controller), 638 new GridBagConstraints(1, 1, 1, 1, 0, 0, 639 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(1, 1, 1, 1), 0, 0)); 640 navigationPanel.add(new NavigationButton(0, 0, (float)Math.PI / 100, "TURN_DOWN", preferences, controller), 641 new GridBagConstraints(1, 2, 1, 1, 0, 0, 642 GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 1, 0), 0, 0)); 643 return navigationPanel; 644 } 645 646 /** 647 * An icon button that changes camera location and angles when pressed. 648 */ 649 private static class NavigationButton extends JButton { 650 private boolean shiftDown; 651 NavigationButton(final float moveDelta, final float yawDelta, final float pitchDelta, String actionName, UserPreferences preferences, final HomeController3D controller)652 public NavigationButton(final float moveDelta, 653 final float yawDelta, 654 final float pitchDelta, 655 String actionName, 656 UserPreferences preferences, 657 final HomeController3D controller) { 658 super(new ResourceAction(preferences, HomeComponent3D.class, actionName, true) { 659 @Override 660 public void actionPerformed(ActionEvent ev) { 661 // Manage auto repeat button with mouse listener 662 } 663 }); 664 // Create a darker press icon 665 setPressedIcon(new ImageIcon(createImage(new FilteredImageSource( 666 ((ImageIcon)getIcon()).getImage().getSource(), 667 new RGBImageFilter() { 668 { 669 canFilterIndexColorModel = true; 670 } 671 672 public int filterRGB (int x, int y, int rgb) { 673 // Return darker color 674 int alpha = rgb & 0xFF000000; 675 int darkerRed = ((rgb & 0xFF0000) >> 1) & 0xFF0000; 676 int darkerGreen = ((rgb & 0x00FF00) >> 1) & 0x00FF00; 677 int darkerBlue = (rgb & 0x0000FF) >> 1; 678 return alpha | darkerRed | darkerGreen | darkerBlue; 679 } 680 })))); 681 682 // Track shift key press 683 addMouseMotionListener(new MouseMotionAdapter() { 684 @Override 685 public void mouseDragged(MouseEvent ev) { 686 shiftDown = ev.isShiftDown(); 687 } 688 }); 689 addMouseListener(new MouseAdapter() { 690 @Override 691 public void mousePressed(MouseEvent ev) { 692 shiftDown = ev.isShiftDown(); 693 SwingUtilities.getAncestorOfClass(HomeComponent3D.class, 694 NavigationButton.this).requestFocusInWindow(); 695 } 696 }); 697 698 // Create a timer that will update camera angles and location 699 final Timer timer = new Timer(50, new ActionListener() { 700 public void actionPerformed(ActionEvent ev) { 701 controller.moveCamera(shiftDown ? moveDelta : moveDelta / 5); 702 controller.rotateCameraYaw(shiftDown ? yawDelta : yawDelta / 5); 703 controller.rotateCameraPitch(pitchDelta); 704 } 705 }); 706 timer.setInitialDelay(0); 707 708 // Update camera when button is armed 709 addChangeListener(new ChangeListener() { 710 public void stateChanged(ChangeEvent ev) { 711 if (getModel().isArmed() 712 && !timer.isRunning()) { 713 timer.restart(); 714 } else if (!getModel().isArmed() 715 && timer.isRunning()) { 716 timer.stop(); 717 } 718 } 719 }); 720 setFocusable(false); 721 setBorder(null); 722 setContentAreaFilled(false); 723 // Force preferred size to ensure button isn't larger 724 setPreferredSize(new Dimension(getIcon().getIconWidth(), getIcon().getIconHeight())); 725 addPropertyChangeListener(JButton.ICON_CHANGED_PROPERTY, new PropertyChangeListener() { 726 public void propertyChange(PropertyChangeEvent ev) { 727 // Reset border when icon is reset after a resource action change 728 setBorder(null); 729 } 730 }); 731 } 732 } 733 734 /** 735 * Sets the component that will be drawn upon the heavyweight 3D component shown by this component. 736 * Mouse events will targeted to the navigation panel when needed. 737 * Supports transparent components. 738 */ setNavigationPanelVisible(boolean visible)739 private void setNavigationPanelVisible(boolean visible) { 740 if (this.navigationPanel != null) { 741 this.navigationPanel.setVisible(visible); 742 if (visible) { 743 // Add a component listener that updates navigation panel image 744 this.navigationPanelListener = new ComponentAdapter() { 745 @Override 746 public void componentResized(ComponentEvent ev) { 747 updateNavigationPanelImage(); 748 } 749 750 @Override 751 public void componentMoved(ComponentEvent e) { 752 updateNavigationPanelImage(); 753 } 754 }; 755 this.navigationPanel.addComponentListener(this.navigationPanelListener); 756 // Add the navigation panel to this component to be able to paint it 757 // but show it behind canvas 3D 758 this.component3D.getParent().add(this.navigationPanel); 759 } else { 760 this.navigationPanel.removeComponentListener(this.navigationPanelListener); 761 if (this.navigationPanel.getParent() != null) { 762 this.navigationPanel.getParent().remove(this.navigationPanel); 763 } 764 } 765 revalidate(); 766 updateNavigationPanelImage(); 767 this.component3D.repaint(); 768 } 769 } 770 771 /** 772 * Updates the image of the components that may overlap canvas 3D 773 * (with a Z order smaller than the one of the canvas 3D). 774 */ updateNavigationPanelImage()775 private void updateNavigationPanelImage() { 776 if (this.navigationPanel != null 777 && this.navigationPanel.isVisible()) { 778 Rectangle componentBounds = this.navigationPanel.getBounds(); 779 Rectangle imageSize = new Rectangle(this.component3D.getX(), this.component3D.getY()); 780 imageSize.add(componentBounds.x + componentBounds.width, 781 componentBounds.y + componentBounds.height); 782 if (!imageSize.isEmpty()) { 783 BufferedImage updatedImage = this.navigationPanelImage; 784 // Consider that no navigation panel image is available 785 // while it's updated 786 this.navigationPanelImage = null; 787 Graphics2D g2D; 788 if (updatedImage == null 789 || updatedImage.getWidth() != imageSize.width 790 || updatedImage.getHeight() != imageSize.height) { 791 updatedImage = new BufferedImage( 792 imageSize.width, imageSize.height, BufferedImage.TYPE_INT_ARGB); 793 g2D = (Graphics2D)updatedImage.getGraphics(); 794 } else { 795 // Clear image 796 g2D = (Graphics2D)updatedImage.getGraphics(); 797 Composite oldComposite = g2D.getComposite(); 798 g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0)); 799 g2D.fill(new Rectangle2D.Double(0, 0, imageSize.width, imageSize.height)); 800 g2D.setComposite(oldComposite); 801 } 802 this.navigationPanel.paintAll(g2D); 803 g2D.dispose(); 804 // Navigation panel image ready to be displayed 805 this.navigationPanelImage = updatedImage; 806 return; 807 } 808 } 809 this.navigationPanelImage = null; 810 } 811 812 /** 813 * Returns a new 3D universe that displays <code>home</code> objects. 814 */ createUniverse(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)815 private SimpleUniverse createUniverse(boolean displayShadowOnFloor, 816 boolean listenToHomeUpdates, 817 boolean waitForLoading) { 818 // Create a universe bound to no canvas 3D 819 ViewingPlatform viewingPlatform = new ViewingPlatform(); 820 // Add an interpolator to view transform to get smooth transition 821 TransformGroup viewPlatformTransform = viewingPlatform.getViewPlatformTransform(); 822 CameraInterpolator cameraInterpolator = new CameraInterpolator(viewPlatformTransform); 823 cameraInterpolator.setSchedulingBounds(new BoundingSphere(new Point3d(), 1E7)); 824 viewPlatformTransform.addChild(cameraInterpolator); 825 viewPlatformTransform.setCapability(TransformGroup.ALLOW_CHILDREN_READ); 826 827 Viewer viewer = new Viewer(new Canvas3D [0]); 828 SimpleUniverse universe = new SimpleUniverse(viewingPlatform, viewer); 829 830 View view = viewer.getView(); 831 view.setTransparencySortingPolicy(View.TRANSPARENCY_SORT_GEOMETRY); 832 833 // Update field of view from current camera 834 updateView(view, this.home.getCamera()); 835 836 // Update point of view from current camera 837 updateViewPlatformTransform(viewPlatformTransform, this.home.getCamera(), false); 838 839 // Add camera listeners to update later point of view from camera 840 if (listenToHomeUpdates) { 841 addCameraListeners(view, viewPlatformTransform); 842 } 843 844 // Link scene matching home to universe 845 universe.addBranchGraph(createSceneTree( 846 displayShadowOnFloor, listenToHomeUpdates, waitForLoading)); 847 848 return universe; 849 } 850 851 /** 852 * Remove all listeners bound to home that updates 3D scene objects. 853 */ removeHomeListeners()854 private void removeHomeListeners() { 855 this.home.removePropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener); 856 HomeEnvironment homeEnvironment = this.home.getEnvironment(); 857 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_COLOR, this.backgroundChangeListener); 858 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_TEXTURE, this.backgroundChangeListener); 859 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_COLOR, this.backgroundChangeListener); 860 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_TEXTURE, this.backgroundChangeListener); 861 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_COLOR, this.groundChangeListener); 862 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_TEXTURE, this.groundChangeListener); 863 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D, this.groundChangeListener); 864 this.home.removePropertyChangeListener(Home.Property.BACKGROUND_IMAGE, this.groundChangeListener); 865 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.LIGHT_COLOR, this.backgroundLightColorListener); 866 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener); 867 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener); 868 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener); 869 homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SUBPART_SIZE_UNDER_LIGHT, this.subpartSizeListener); 870 this.home.getCamera().removePropertyChangeListener(this.cameraChangeListener); 871 this.home.removePropertyChangeListener(Home.Property.CAMERA, this.elevationChangeListener); 872 this.home.getCamera().removePropertyChangeListener(this.elevationChangeListener); 873 this.home.removeLevelsListener(this.levelListener); 874 for (Level level : this.home.getLevels()) { 875 level.removePropertyChangeListener(this.levelChangeListener); 876 } 877 this.home.removeWallsListener(this.wallListener); 878 for (Wall wall : this.home.getWalls()) { 879 wall.removePropertyChangeListener(this.wallChangeListener); 880 } 881 this.home.removeFurnitureListener(this.furnitureListener); 882 for (HomePieceOfFurniture piece : this.home.getFurniture()) { 883 removePropertyChangeListener(piece, this.furnitureChangeListener); 884 } 885 this.home.removeRoomsListener(this.roomListener); 886 for (Room room : this.home.getRooms()) { 887 room.removePropertyChangeListener(this.roomChangeListener); 888 } 889 this.home.removePolylinesListener(this.polylineListener); 890 for (Polyline polyline : this.home.getPolylines()) { 891 polyline.removePropertyChangeListener(this.polylineChangeListener); 892 } 893 this.home.removeLabelsListener(this.labelListener); 894 for (Label label : this.home.getLabels()) { 895 label.removePropertyChangeListener(this.labelChangeListener); 896 } 897 } 898 899 /** 900 * Prints this component to make it fill <code>pageFormat</code> imageable size. 901 */ print(Graphics g, PageFormat pageFormat, int pageIndex)902 public int print(Graphics g, PageFormat pageFormat, int pageIndex) { 903 if (pageIndex == 0) { 904 // Compute printed image size to render 3D view in 150 dpi 905 double printSize = Math.min(pageFormat.getImageableWidth(), 906 pageFormat.getImageableHeight()); 907 int printedImageSize = (int)(printSize / 72 * 150); 908 if (this.printedImageCache == null 909 || this.printedImageCache.getWidth() != printedImageSize) { 910 try { 911 this.printedImageCache = getOffScreenImage(printedImageSize, printedImageSize); 912 } catch (IllegalRenderingStateException ex) { 913 // If off screen canvas failed, consider that 3D view page doesn't exist 914 return NO_SUCH_PAGE; 915 } 916 } 917 918 Graphics2D g2D = (Graphics2D)g.create(); 919 // Center the 3D view in component 920 g2D.translate(pageFormat.getImageableX() + (pageFormat.getImageableWidth() - printSize) / 2, 921 pageFormat.getImageableY() + (pageFormat.getImageableHeight() - printSize) / 2); 922 double scale = printSize / printedImageSize; 923 g2D.scale(scale, scale); 924 g2D.drawImage(this.printedImageCache, 0, 0, this); 925 g2D.dispose(); 926 927 return PAGE_EXISTS; 928 } else { 929 return NO_SUCH_PAGE; 930 } 931 } 932 933 /** 934 * Optimizes this component for the creation of a sequence of multiple off screen images. 935 * Once off screen images are generated with {@link #getOffScreenImage(int, int) getOffScreenImage}, 936 * call {@link #endOffscreenImagesCreation() endOffscreenImagesCreation} method to free resources. 937 */ startOffscreenImagesCreation()938 public void startOffscreenImagesCreation() { 939 if (this.offscreenUniverse == null) { 940 if (this.onscreenUniverse != null) { 941 throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time"); 942 } 943 this.offscreenUniverse = createUniverse(this.displayShadowOnFloor, true, true); 944 // Replace textures by clones because Java 3D doesn't accept all the time 945 // to share textures between offscreen and onscreen environments 946 Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>(); 947 for (Enumeration it = this.offscreenUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) { 948 cloneTexture((Node)it.nextElement(), replacedTextures); 949 } 950 } 951 } 952 953 /** 954 * Returns an image of the home viewed by this component at the given size. 955 */ getOffScreenImage(int width, int height)956 public BufferedImage getOffScreenImage(int width, int height) { 957 List<Selectable> selectedItems = this.home.getSelectedItems(); 958 SimpleUniverse offScreenImageUniverse = null; 959 try { 960 View view; 961 if (this.offscreenUniverse == null) { 962 offScreenImageUniverse = createUniverse(this.displayShadowOnFloor, false, true); 963 view = offScreenImageUniverse.getViewer().getView(); 964 // Replace textures by clones because Java 3D doesn't accept all the time 965 // to share textures between offscreen and onscreen environments 966 Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>(); 967 for (Enumeration it = offScreenImageUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) { 968 cloneTexture((Node)it.nextElement(), replacedTextures); 969 } 970 } else { 971 view = this.offscreenUniverse.getViewer().getView(); 972 } 973 974 updateView(view, this.home.getCamera(), width, height); 975 976 // Empty temporarily selection to create the off screen image 977 List<Selectable> emptySelection = Collections.emptyList(); 978 this.home.setSelectedItems(emptySelection); 979 return Component3DManager.getInstance().getOffScreenImage(view, width, height); 980 } finally { 981 // Restore selection 982 this.home.setSelectedItems(selectedItems); 983 if (offScreenImageUniverse != null) { 984 offScreenImageUniverse.cleanup(); 985 } 986 } 987 } 988 989 /** 990 * Replace the textures set on node shapes by clones. 991 */ cloneTexture(Node node, Map<Texture, Texture> replacedTextures)992 private void cloneTexture(Node node, Map<Texture, Texture> replacedTextures) { 993 if (node instanceof Group) { 994 // Enumerate children 995 Enumeration<?> enumeration = ((Group)node).getAllChildren(); 996 while (enumeration.hasMoreElements()) { 997 cloneTexture((Node)enumeration.nextElement(), replacedTextures); 998 } 999 } else if (node instanceof Link) { 1000 cloneTexture(((Link)node).getSharedGroup(), replacedTextures); 1001 } else if (node instanceof Shape3D) { 1002 Appearance appearance = ((Shape3D)node).getAppearance(); 1003 if (appearance != null) { 1004 Texture texture = appearance.getTexture(); 1005 if (texture != null) { 1006 Texture replacedTexture = replacedTextures.get(texture); 1007 if (replacedTexture == null) { 1008 replacedTexture = (Texture)texture.cloneNodeComponent(false); 1009 replacedTextures.put(texture, replacedTexture); 1010 } 1011 appearance.setTexture(replacedTexture); 1012 } 1013 } 1014 } 1015 } 1016 1017 /** 1018 * Frees unnecessary resources after the creation of a sequence of multiple offscreen images. 1019 */ endOffscreenImagesCreation()1020 public void endOffscreenImagesCreation() { 1021 if (this.offscreenUniverse != null) { 1022 this.offscreenUniverse.cleanup(); 1023 removeHomeListeners(); 1024 this.offscreenUniverse = null; 1025 } 1026 } 1027 1028 /** 1029 * Adds listeners to home to update point of view from current camera. 1030 */ addCameraListeners(final View view, final TransformGroup viewPlatformTransform)1031 private void addCameraListeners(final View view, 1032 final TransformGroup viewPlatformTransform) { 1033 this.cameraChangeListener = new PropertyChangeListener() { 1034 private Runnable updater; 1035 public void propertyChange(PropertyChangeEvent ev) { 1036 if (this.updater == null) { 1037 // Update view transform later to avoid flickering in case of multiple camera changes 1038 EventQueue.invokeLater(this.updater = new Runnable () { 1039 public void run() { 1040 updateView(view, home.getCamera()); 1041 updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), true); 1042 updater = null; 1043 } 1044 }); 1045 } 1046 } 1047 }; 1048 this.home.getCamera().addPropertyChangeListener(this.cameraChangeListener); 1049 this.homeCameraListener = new PropertyChangeListener() { 1050 public void propertyChange(PropertyChangeEvent ev) { 1051 updateView(view, home.getCamera()); 1052 updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), false); 1053 // Add camera change listener to new active camera 1054 ((Camera)ev.getOldValue()).removePropertyChangeListener(cameraChangeListener); 1055 home.getCamera().addPropertyChangeListener(cameraChangeListener); 1056 } 1057 }; 1058 this.home.addPropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener); 1059 } 1060 1061 /** 1062 * Updates <code>view</code> from <code>camera</code> field of view. 1063 */ updateView(View view, Camera camera)1064 private void updateView(View view, Camera camera) { 1065 if (this.component3D != null) { 1066 updateView(view, camera, this.component3D.getWidth(), this.component3D.getHeight()); 1067 } else { 1068 updateView(view, camera, 0, 0); 1069 } 1070 } 1071 updateView(View view, Camera camera, int width, int height)1072 private void updateView(View view, Camera camera, int width, int height) { 1073 float fieldOfView = camera.getFieldOfView(); 1074 if (fieldOfView == 0) { 1075 fieldOfView = (float)(Math.PI * 63 / 180); 1076 } 1077 view.setFieldOfView(fieldOfView); 1078 double frontClipDistance = 2.5f; 1079 float frontBackDistanceRatio = 500000; // More than 10 km for a 2.5 cm front distance 1080 if (Component3DManager.getInstance().getDepthSize() <= 16) { 1081 // It's recommended to keep ratio between back and front clip distances under 3000 for a 16 bit Z-buffer 1082 frontBackDistanceRatio = 3000; 1083 BoundingBox approximateHomeBounds = getApproximateHomeBounds(); 1084 // If camera is out of home bounds, adjust the front clip distance to the distance to home bounds 1085 if (approximateHomeBounds != null 1086 && !approximateHomeBounds.intersect(new Point3d(camera.getX(), camera.getY(), camera.getZ()))) { 1087 float distanceToClosestBoxSide = getDistanceToBox(camera.getX(), camera.getY(), camera.getZ(), approximateHomeBounds); 1088 if (!Float.isNaN(distanceToClosestBoxSide)) { 1089 frontClipDistance = Math.max(frontClipDistance, 0.1f * distanceToClosestBoxSide); 1090 } 1091 } 1092 } 1093 if (camera.getZ() > 0 && width != 0 && height != 0) { 1094 float halfVerticalFieldOfView = (float)Math.atan(Math.tan(fieldOfView / 2) * height / width); 1095 float fieldOfViewBottomAngle = camera.getPitch() + halfVerticalFieldOfView; 1096 // If the horizon is above the frustrum bottom, take into account the distance to the ground 1097 if (fieldOfViewBottomAngle > 0) { 1098 float distanceToGroundAtFieldOfViewBottomAngle = (float)(camera.getZ() / Math.sin(fieldOfViewBottomAngle)); 1099 frontClipDistance = Math.min(frontClipDistance, 0.35f * distanceToGroundAtFieldOfViewBottomAngle); 1100 if (frontClipDistance * frontBackDistanceRatio < distanceToGroundAtFieldOfViewBottomAngle) { 1101 // Ensure the ground is always visible at the back clip distance 1102 frontClipDistance = distanceToGroundAtFieldOfViewBottomAngle / frontBackDistanceRatio; 1103 } 1104 } 1105 } 1106 // Update front and back clip distance 1107 view.setFrontClipDistance(frontClipDistance); 1108 view.setBackClipDistance(frontClipDistance * frontBackDistanceRatio); 1109 clearPrintedImageCache(); 1110 } 1111 1112 /** 1113 * Returns quickly computed bounds of the objects in home. 1114 */ getApproximateHomeBounds()1115 private BoundingBox getApproximateHomeBounds() { 1116 if (this.approximateHomeBoundsCache == null) { 1117 BoundingBox approximateHomeBounds = null; 1118 for (HomePieceOfFurniture piece : this.home.getFurniture()) { 1119 if (piece.isVisible() 1120 && (piece.getLevel() == null 1121 || piece.getLevel().isViewable())) { 1122 float halfMaxDimension = Math.max(piece.getWidthInPlan(), piece.getDepthInPlan()) / 2; 1123 float elevation = piece.getGroundElevation(); 1124 Point3d pieceLocation = new Point3d( 1125 piece.getX() - halfMaxDimension, piece.getY() - halfMaxDimension, elevation); 1126 if (approximateHomeBounds == null) { 1127 approximateHomeBounds = new BoundingBox(pieceLocation, pieceLocation); 1128 } else { 1129 approximateHomeBounds.combine(pieceLocation); 1130 } 1131 approximateHomeBounds.combine(new Point3d( 1132 piece.getX() + halfMaxDimension, piece.getY() + halfMaxDimension, elevation + piece.getHeightInPlan())); 1133 } 1134 } 1135 for (Wall wall : this.home.getWalls()) { 1136 if (wall.getLevel() == null 1137 || wall.getLevel().isViewable()) { 1138 Point3d startPoint = new Point3d(wall.getXStart(), wall.getYStart(), 1139 wall.getLevel() != null ? wall.getLevel().getElevation() : 0); 1140 if (approximateHomeBounds == null) { 1141 approximateHomeBounds = new BoundingBox(startPoint, startPoint); 1142 } else { 1143 approximateHomeBounds.combine(startPoint); 1144 } 1145 approximateHomeBounds.combine(new Point3d(wall.getXEnd(), wall.getYEnd(), 1146 startPoint.z + (wall.getHeight() != null ? wall.getHeight() : this.home.getWallHeight()))); 1147 } 1148 } 1149 for (Room room : this.home.getRooms()) { 1150 if (room.getLevel() == null 1151 || room.getLevel().isViewable()) { 1152 Point3d center = new Point3d(room.getXCenter(), room.getYCenter(), 1153 room.getLevel() != null ? room.getLevel().getElevation() : 0); 1154 if (approximateHomeBounds == null) { 1155 approximateHomeBounds = new BoundingBox(center, center); 1156 } else { 1157 approximateHomeBounds.combine(center); 1158 } 1159 } 1160 } 1161 for (Label label : this.home.getLabels()) { 1162 if ((label.getLevel() == null 1163 || label.getLevel().isViewable()) 1164 && label.getPitch() != null) { 1165 Point3d center = new Point3d(label.getX(), label.getY(), label.getGroundElevation()); 1166 if (approximateHomeBounds == null) { 1167 approximateHomeBounds = new BoundingBox(center, center); 1168 } else { 1169 approximateHomeBounds.combine(center); 1170 } 1171 } 1172 } 1173 this.approximateHomeBoundsCache = approximateHomeBounds; 1174 } 1175 return this.approximateHomeBoundsCache; 1176 } 1177 1178 /** 1179 * Returns the distance between the point at the given coordinates (x,y,z) and the closest side of <code>box</code>. 1180 */ getDistanceToBox(float x, float y, float z, BoundingBox box)1181 private float getDistanceToBox(float x, float y, float z, BoundingBox box) { 1182 Point3f point = new Point3f(x, y, z); 1183 Point3d lower = new Point3d(); 1184 box.getLower(lower); 1185 Point3d upper = new Point3d(); 1186 box.getUpper(upper); 1187 Point3f [] boxVertices = { 1188 new Point3f((float)lower.x, (float)lower.y, (float)lower.z), 1189 new Point3f((float)upper.x, (float)lower.y, (float)lower.z), 1190 new Point3f((float)lower.x, (float)upper.y, (float)lower.z), 1191 new Point3f((float)upper.x, (float)upper.y, (float)lower.z), 1192 new Point3f((float)lower.x, (float)lower.y, (float)upper.z), 1193 new Point3f((float)upper.x, (float)lower.y, (float)upper.z), 1194 new Point3f((float)lower.x, (float)upper.y, (float)upper.z), 1195 new Point3f((float)upper.x, (float)upper.y, (float)upper.z)}; 1196 float [] distancesToVertex = new float [boxVertices.length]; 1197 for (int i = 0; i < distancesToVertex.length; i++) { 1198 distancesToVertex [i] = point.distanceSquared(boxVertices [i]); 1199 } 1200 float [] distancesToSide = { 1201 getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 3, 2, 2), 1202 getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 5, 4, 1), 1203 getDistanceToSide(point, boxVertices, distancesToVertex, 0, 2, 6, 4, 0), 1204 getDistanceToSide(point, boxVertices, distancesToVertex, 4, 5, 7, 6, 2), 1205 getDistanceToSide(point, boxVertices, distancesToVertex, 2, 3, 7, 6, 1), 1206 getDistanceToSide(point, boxVertices, distancesToVertex, 1, 3, 7, 5, 0)}; 1207 float distance = distancesToSide [0]; 1208 for (int i = 1; i < distancesToSide.length; i++) { 1209 distance = Math.min(distance, distancesToSide [i]); 1210 } 1211 return distance; 1212 } 1213 1214 /** 1215 * Returns the distance between the given <code>point</code> and the plane defined by four vertices. 1216 */ getDistanceToSide(Point3f point, Point3f [] boxVertices, float [] distancesSquaredToVertex, int index1, int index2, int index3, int index4, int axis)1217 private float getDistanceToSide(Point3f point, Point3f [] boxVertices, float [] distancesSquaredToVertex, 1218 int index1, int index2, int index3, int index4, int axis) { 1219 switch (axis) { 1220 case 0 : // Normal along x axis 1221 if (point.y <= boxVertices [index1].y) { 1222 if (point.z <= boxVertices [index1].z) { 1223 return (float)Math.sqrt(distancesSquaredToVertex [index1]); 1224 } else if (point.z >= boxVertices [index4].z) { 1225 return (float)Math.sqrt(distancesSquaredToVertex [index4]); 1226 } else { 1227 return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]); 1228 } 1229 } else if (point.y >= boxVertices [index2].y) { 1230 if (point.z <= boxVertices [index2].z) { 1231 return (float)Math.sqrt(distancesSquaredToVertex [index2]); 1232 } else if (point.z >= boxVertices [index3].z) { 1233 return (float)Math.sqrt(distancesSquaredToVertex [index3]); 1234 } else { 1235 return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]); 1236 } 1237 } else if (point.z <= boxVertices [index1].z) { 1238 return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]); 1239 } else if (point.z >= boxVertices [index4].z) { 1240 return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]); 1241 } 1242 break; 1243 case 1 : // Normal along y axis 1244 if (point.x <= boxVertices [index1].x) { 1245 if (point.z <= boxVertices [index1].z) { 1246 return (float)Math.sqrt(distancesSquaredToVertex [index1]); 1247 } else if (point.z >= boxVertices [index4].z) { 1248 return (float)Math.sqrt(distancesSquaredToVertex [index4]); 1249 } else { 1250 return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]); 1251 } 1252 } else if (point.x >= boxVertices [index2].x) { 1253 if (point.z <= boxVertices [index2].z) { 1254 return (float)Math.sqrt(distancesSquaredToVertex [index2]); 1255 } else if (point.z >= boxVertices [index3].z) { 1256 return (float)Math.sqrt(distancesSquaredToVertex [index3]); 1257 } else { 1258 return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]); 1259 } 1260 } else if (point.z <= boxVertices [index1].z) { 1261 return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]); 1262 } else if (point.z >= boxVertices [index4].z) { 1263 return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]); 1264 } 1265 break; 1266 case 2 : // Normal along z axis 1267 if (point.x <= boxVertices [index1].x) { 1268 if (point.y <= boxVertices [index1].y) { 1269 return (float)Math.sqrt(distancesSquaredToVertex [index1]); 1270 } else if (point.y >= boxVertices [index4].y) { 1271 return (float)Math.sqrt(distancesSquaredToVertex [index4]); 1272 } else { 1273 return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]); 1274 } 1275 } else if (point.x >= boxVertices [index2].x) { 1276 if (point.y <= boxVertices [index2].y) { 1277 return (float)Math.sqrt(distancesSquaredToVertex [index2]); 1278 } else if (point.y >= boxVertices [index3].y) { 1279 return (float)Math.sqrt(distancesSquaredToVertex [index3]); 1280 } else { 1281 return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]); 1282 } 1283 } else if (point.y <= boxVertices [index1].y) { 1284 return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]); 1285 } else if (point.y >= boxVertices [index4].y) { 1286 return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]); 1287 } 1288 break; 1289 } 1290 1291 // Return distance to plane 1292 // from https://fr.wikipedia.org/wiki/Distance_d%27un_point_�_un_plan 1293 Vector3f vector1 = new Vector3f(boxVertices [index2].x - boxVertices [index1].x, 1294 boxVertices [index2].y - boxVertices [index1].y, 1295 boxVertices [index2].z - boxVertices [index1].z); 1296 Vector3f vector2 = new Vector3f(boxVertices [index3].x - boxVertices [index1].x, 1297 boxVertices [index3].y - boxVertices [index1].y, 1298 boxVertices [index3].z - boxVertices [index1].z); 1299 Vector3f normal = new Vector3f(); 1300 normal.cross(vector1, vector2); 1301 return Math.abs(normal.dot(new Vector3f(boxVertices [index1].x - point.x, boxVertices [index1].y - point.y, boxVertices [index1].z - point.z))) / 1302 normal.length(); 1303 } 1304 1305 /** 1306 * Returns the distance between the given <code>point</code> and the line defined by two points. 1307 */ getDistanceToLine(Point3f point, Point3f point1, Point3f point2)1308 private float getDistanceToLine(Point3f point, Point3f point1, Point3f point2) { 1309 // From https://fr.wikipedia.org/wiki/Distance_d%27un_point_�_une_droite#Dans_l.27espace 1310 Vector3f lineDirection = new Vector3f(point2.x - point1.x, point2.y - point1.y, point2.z - point1.z); 1311 Vector3f vector = new Vector3f(point.x - point1.x, point.y - point1.y, point.z - point1.z); 1312 Vector3f crossProduct = new Vector3f(); 1313 crossProduct.cross(lineDirection, vector); 1314 return crossProduct.length() / lineDirection.length(); 1315 } 1316 1317 /** 1318 * Frees printed image kept in cache. 1319 */ clearPrintedImageCache()1320 private void clearPrintedImageCache() { 1321 this.printedImageCache = null; 1322 } 1323 1324 /** 1325 * Updates <code>viewPlatformTransform</code> transform from <code>camera</code> angles and location. 1326 */ updateViewPlatformTransform(TransformGroup viewPlatformTransform, Camera camera, boolean updateWithAnimation)1327 private void updateViewPlatformTransform(TransformGroup viewPlatformTransform, 1328 Camera camera, boolean updateWithAnimation) { 1329 // Get the camera interpolator 1330 CameraInterpolator cameraInterpolator = 1331 (CameraInterpolator)viewPlatformTransform.getChild(viewPlatformTransform.numChildren() - 1); 1332 if (updateWithAnimation) { 1333 cameraInterpolator.moveCamera(camera); 1334 } else { 1335 cameraInterpolator.stop(); 1336 Transform3D transform = new Transform3D(); 1337 updateViewPlatformTransform(transform, camera.getX(), camera.getY(), 1338 camera.getZ(), camera.getYaw(), camera.getPitch()); 1339 viewPlatformTransform.setTransform(transform); 1340 } 1341 clearPrintedImageCache(); 1342 } 1343 1344 /** 1345 * An interpolator that computes smooth camera moves. 1346 */ 1347 private class CameraInterpolator extends TransformInterpolator { 1348 private final ScheduledExecutorService scheduledExecutor; 1349 private Camera initialCamera; 1350 private Camera finalCamera; 1351 CameraInterpolator(TransformGroup transformGroup)1352 public CameraInterpolator(TransformGroup transformGroup) { 1353 this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); 1354 setTarget(transformGroup); 1355 } 1356 1357 /** 1358 * Moves the camera to a new location. 1359 */ moveCamera(Camera finalCamera)1360 public void moveCamera(Camera finalCamera) { 1361 if (this.finalCamera == null 1362 || this.finalCamera.getX() != finalCamera.getX() 1363 || this.finalCamera.getY() != finalCamera.getY() 1364 || this.finalCamera.getZ() != finalCamera.getZ() 1365 || this.finalCamera.getYaw() != finalCamera.getYaw() 1366 || this.finalCamera.getPitch() != finalCamera.getPitch()) { 1367 synchronized (this) { 1368 Alpha alpha = getAlpha(); 1369 if (alpha == null || alpha.finished()) { 1370 this.initialCamera = new Camera(camera.getX(), camera.getY(), camera.getZ(), 1371 camera.getYaw(), camera.getPitch(), camera.getFieldOfView()); 1372 } else if (alpha.value() < 0.3) { 1373 Transform3D finalTransformation = new Transform3D(); 1374 // Jump directly to final location 1375 updateViewPlatformTransform(finalTransformation, this.finalCamera.getX(), this.finalCamera.getY(), this.finalCamera.getZ(), 1376 this.finalCamera.getYaw(), this.finalCamera.getPitch()); 1377 getTarget().setTransform(finalTransformation); 1378 this.initialCamera = this.finalCamera; 1379 } else { 1380 // Compute initial location from current alpha value 1381 this.initialCamera = new Camera(this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha.value(), 1382 this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha.value(), 1383 this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha.value(), 1384 this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha.value(), 1385 this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha.value(), 1386 finalCamera.getFieldOfView()); 1387 } 1388 this.finalCamera = new Camera(finalCamera.getX(), finalCamera.getY(), finalCamera.getZ(), 1389 finalCamera.getYaw(), finalCamera.getPitch(), finalCamera.getFieldOfView()); 1390 1391 // Create an animation that will interpolate camera location 1392 // between initial camera and final camera in 150 ms 1393 if (alpha == null) { 1394 alpha = new Alpha(1, 150); 1395 setAlpha(alpha); 1396 } 1397 // Start animation now 1398 alpha.setStartTime(System.currentTimeMillis()); 1399 // In case system is overloaded computeTransform won't be called 1400 // ensure final location will always be set after 150 ms 1401 this.scheduledExecutor.schedule(new Runnable() { 1402 public void run() { 1403 if (getAlpha().value() == 1) { 1404 Transform3D transform = new Transform3D(); 1405 computeTransform(1, transform); 1406 getTarget().setTransform(transform); 1407 } 1408 } 1409 }, 150, TimeUnit.MILLISECONDS); 1410 } 1411 } 1412 } 1413 1414 @Override computeTransform(float alpha, Transform3D transform)1415 public synchronized void computeTransform(float alpha, Transform3D transform) { 1416 updateViewPlatformTransform(transform, 1417 this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha, 1418 this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha, 1419 this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha, 1420 this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha, 1421 this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha); 1422 } 1423 stop()1424 public synchronized void stop() { 1425 setAlpha(null); 1426 this.finalCamera = null; 1427 } 1428 } 1429 1430 /** 1431 * Updates <code>viewPlatformTransform</code> transform from camera angles and location. 1432 */ updateViewPlatformTransform(Transform3D transform, float cameraX, float cameraY, float cameraZ, float cameraYaw, float cameraPitch)1433 private void updateViewPlatformTransform(Transform3D transform, 1434 float cameraX, float cameraY, float cameraZ, 1435 float cameraYaw, float cameraPitch) { 1436 Transform3D yawRotation = new Transform3D(); 1437 yawRotation.rotY(-cameraYaw + Math.PI); 1438 1439 Transform3D pitchRotation = new Transform3D(); 1440 pitchRotation.rotX(-cameraPitch); 1441 yawRotation.mul(pitchRotation); 1442 1443 transform.setIdentity(); 1444 transform.setTranslation(new Vector3f(cameraX, cameraZ, cameraY)); 1445 transform.mul(yawRotation); 1446 1447 this.camera = new Camera(cameraX, cameraY, cameraZ, cameraYaw, cameraPitch, 0); 1448 } 1449 1450 /** 1451 * Adds AWT mouse listeners to <code>component3D</code> that calls back <code>controller</code> methods. 1452 */ addMouseListeners(final HomeController3D controller, final Component component3D)1453 private void addMouseListeners(final HomeController3D controller, final Component component3D) { 1454 MouseInputAdapter mouseListener = new MouseInputAdapter() { 1455 private int xLastMouseMove; 1456 private int yLastMouseMove; 1457 private Component grabComponent; 1458 private Component previousMouseEventTarget; 1459 1460 @Override 1461 public void mousePressed(MouseEvent ev) { 1462 if (!retargetMouseEventToNavigationPanelChildren(ev)) { 1463 if (ev.isPopupTrigger()) { 1464 mouseReleased(ev); 1465 } else if (isEnabled()) { 1466 requestFocusInWindow(); 1467 this.xLastMouseMove = ev.getX(); 1468 this.yLastMouseMove = ev.getY(); 1469 } 1470 } 1471 } 1472 1473 @Override 1474 public void mouseClicked(MouseEvent ev) { 1475 retargetMouseEventToNavigationPanelChildren(ev); 1476 } 1477 1478 @Override 1479 public void mouseMoved(MouseEvent ev) { 1480 retargetMouseEventToNavigationPanelChildren(ev); 1481 } 1482 1483 @Override 1484 public void mouseDragged(MouseEvent ev) { 1485 if (!retargetMouseEventToNavigationPanelChildren(ev)) { 1486 if (isEnabled()) { 1487 if (ev.isAltDown()) { 1488 // Mouse move along Y axis while alt is down changes camera location 1489 float delta = 1.25f * (this.yLastMouseMove - ev.getY()); 1490 // Multiply delta by 5 if shift is down 1491 if (ev.isShiftDown()) { 1492 delta *= 5; 1493 } 1494 controller.moveCamera(delta); 1495 } else { 1496 final float ANGLE_FACTOR = 0.005f; 1497 // Mouse move along X axis changes camera yaw 1498 float yawDelta = ANGLE_FACTOR * (ev.getX() - this.xLastMouseMove); 1499 // Multiply yaw delta by 5 if shift is down 1500 if (ev.isShiftDown()) { 1501 yawDelta *= 5; 1502 } 1503 controller.rotateCameraYaw(yawDelta); 1504 1505 // Mouse move along Y axis changes camera pitch 1506 float pitchDelta = ANGLE_FACTOR * (ev.getY() - this.yLastMouseMove); 1507 controller.rotateCameraPitch(pitchDelta); 1508 } 1509 1510 this.xLastMouseMove = ev.getX(); 1511 this.yLastMouseMove = ev.getY(); 1512 } 1513 } 1514 } 1515 1516 @Override 1517 public void mouseReleased(MouseEvent ev) { 1518 if (!retargetMouseEventToNavigationPanelChildren(ev)) { 1519 if (ev.isPopupTrigger()) { 1520 JPopupMenu componentPopupMenu = getComponentPopupMenu(); 1521 if (componentPopupMenu != null) { 1522 componentPopupMenu.show(HomeComponent3D.this, ev.getX(), ev.getY()); 1523 } 1524 } 1525 } 1526 } 1527 1528 /** 1529 * Retargets to the first component of navigation panel able to manage the given event 1530 * and returns <code>true</code> if a component consumed the event 1531 * or needs to be repainted (meaning its state changed). 1532 * This implementation doesn't cover all the possible cases (mouseEntered and mouseExited 1533 * events are managed only during mouseDragged event). 1534 */ 1535 private boolean retargetMouseEventToNavigationPanelChildren(MouseEvent ev) { 1536 if (navigationPanel != null 1537 && navigationPanel.isVisible()) { 1538 if (this.grabComponent != null 1539 && (ev.getID() == MouseEvent.MOUSE_RELEASED 1540 || ev.getID() == MouseEvent.MOUSE_DRAGGED)) { 1541 Point point = SwingUtilities.convertPoint(ev.getComponent(), ev.getPoint(), this.grabComponent); 1542 dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, ev.getID(), point.x, point.y)); 1543 if (ev.getID() == MouseEvent.MOUSE_RELEASED) { 1544 this.grabComponent = null; 1545 } else { 1546 if (this.previousMouseEventTarget == null 1547 && this.grabComponent.contains(point)) { 1548 dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_ENTERED, point.x, point.y)); 1549 this.previousMouseEventTarget = this.grabComponent; 1550 } else if (this.previousMouseEventTarget != null 1551 && !this.grabComponent.contains(point)) { 1552 dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_EXITED, point.x, point.y)); 1553 this.previousMouseEventTarget = null; 1554 } 1555 } 1556 return true; 1557 } else { 1558 Component mouseEventTarget = retargetMouseEvent(navigationPanel, ev); 1559 if (mouseEventTarget != null) { 1560 this.previousMouseEventTarget = mouseEventTarget; 1561 return true; 1562 } 1563 } 1564 } 1565 return false; 1566 } 1567 1568 private Component retargetMouseEvent(Component component, MouseEvent ev) { 1569 if (component.getBounds().contains(ev.getPoint())) { 1570 if (component instanceof Container) { 1571 Container container = (Container)component; 1572 for (int i = container.getComponentCount() - 1; i >= 0; i--) { 1573 Component c = container.getComponent(i); 1574 MouseEvent retargetedEvent = deriveEvent(ev, component, ev.getID(), 1575 ev.getX() - component.getX(), ev.getY() - component.getY()); 1576 Component mouseEventTarget = retargetMouseEvent(c, retargetedEvent); 1577 if (mouseEventTarget != null) { 1578 return mouseEventTarget; 1579 } 1580 } 1581 } 1582 int newX = ev.getX() - component.getX(); 1583 int newY = ev.getY() - component.getY(); 1584 if (dispatchRetargetedEvent(deriveEvent(ev, component, ev.getID(), newX, newY))) { 1585 if (ev.getID() == MouseEvent.MOUSE_PRESSED) { 1586 this.grabComponent = component; 1587 } 1588 return component; 1589 } 1590 } 1591 return null; 1592 } 1593 1594 /** 1595 * Dispatches the given event to its component and returns <code>true</code> if component needs to be redrawn. 1596 */ 1597 private boolean dispatchRetargetedEvent(MouseEvent ev) { 1598 ev.getComponent().dispatchEvent(ev); 1599 if (!RepaintManager.currentManager(ev.getComponent()).getDirtyRegion((JComponent)ev.getComponent()).isEmpty()) { 1600 updateNavigationPanelImage(); 1601 component3D.repaint(); 1602 return true; 1603 } 1604 return false; 1605 } 1606 1607 /** 1608 * Returns a new <code>MouseEvent</code> derived from the one given in parameter. 1609 */ 1610 private MouseEvent deriveEvent(MouseEvent ev, Component component, int id, int x, int y) { 1611 return new MouseEvent(component, id, ev.getWhen(), 1612 ev.getModifiersEx() | ev.getModifiers(), x, y, 1613 ev.getClickCount(), ev.isPopupTrigger(), ev.getButton()); 1614 } 1615 }; 1616 MouseWheelListener mouseWheelListener = new MouseWheelListener() { 1617 public void mouseWheelMoved(MouseWheelEvent ev) { 1618 if (isEnabled()) { 1619 // Mouse wheel changes camera location 1620 float delta = -2.5f * ev.getWheelRotation(); 1621 // Multiply delta by 10 if shift is down 1622 if (ev.isShiftDown()) { 1623 delta *= 5; 1624 } 1625 controller.moveCamera(delta); 1626 } 1627 } 1628 }; 1629 1630 component3D.addMouseListener(mouseListener); 1631 component3D.addMouseMotionListener(mouseListener); 1632 component3D.addMouseWheelListener(mouseWheelListener); 1633 // Add a mouse listener to this component to request focus in case user clicks in component border 1634 super.addMouseListener(new MouseInputAdapter() { 1635 @Override 1636 public void mousePressed(MouseEvent e) { 1637 requestFocusInWindow(); 1638 } 1639 }); 1640 } 1641 1642 /** 1643 * Installs keys bound to actions. 1644 */ installKeyboardActions()1645 private void installKeyboardActions() { 1646 InputMap inputMap = getInputMap(WHEN_FOCUSED); 1647 // Tolerate alt modifier for forward and backward moves with UP and DOWN keys to avoid 1648 // the user to release the alt key when he wants to alternate forward/backward and sideways moves 1649 inputMap.put(KeyStroke.getKeyStroke("shift UP"), ActionType.MOVE_CAMERA_FAST_FORWARD); 1650 inputMap.put(KeyStroke.getKeyStroke("shift alt UP"), ActionType.MOVE_CAMERA_FAST_FORWARD); 1651 inputMap.put(KeyStroke.getKeyStroke("shift W"), ActionType.MOVE_CAMERA_FAST_FORWARD); 1652 inputMap.put(KeyStroke.getKeyStroke("UP"), ActionType.MOVE_CAMERA_FORWARD); 1653 inputMap.put(KeyStroke.getKeyStroke("alt UP"), ActionType.MOVE_CAMERA_FORWARD); 1654 inputMap.put(KeyStroke.getKeyStroke("W"), ActionType.MOVE_CAMERA_FORWARD); 1655 inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), ActionType.MOVE_CAMERA_FAST_BACKWARD); 1656 inputMap.put(KeyStroke.getKeyStroke("shift alt DOWN"), ActionType.MOVE_CAMERA_FAST_BACKWARD); 1657 inputMap.put(KeyStroke.getKeyStroke("shift S"), ActionType.MOVE_CAMERA_FAST_BACKWARD); 1658 inputMap.put(KeyStroke.getKeyStroke("DOWN"), ActionType.MOVE_CAMERA_BACKWARD); 1659 inputMap.put(KeyStroke.getKeyStroke("alt DOWN"), ActionType.MOVE_CAMERA_BACKWARD); 1660 inputMap.put(KeyStroke.getKeyStroke("S"), ActionType.MOVE_CAMERA_BACKWARD); 1661 inputMap.put(KeyStroke.getKeyStroke("shift alt LEFT"), ActionType.MOVE_CAMERA_FAST_LEFT); 1662 inputMap.put(KeyStroke.getKeyStroke("alt LEFT"), ActionType.MOVE_CAMERA_LEFT); 1663 inputMap.put(KeyStroke.getKeyStroke("shift alt RIGHT"), ActionType.MOVE_CAMERA_FAST_RIGHT); 1664 inputMap.put(KeyStroke.getKeyStroke("alt RIGHT"), ActionType.MOVE_CAMERA_RIGHT); 1665 inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT); 1666 inputMap.put(KeyStroke.getKeyStroke("shift A"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT); 1667 inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.ROTATE_CAMERA_YAW_LEFT); 1668 inputMap.put(KeyStroke.getKeyStroke("A"), ActionType.ROTATE_CAMERA_YAW_LEFT); 1669 inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT); 1670 inputMap.put(KeyStroke.getKeyStroke("shift D"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT); 1671 inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.ROTATE_CAMERA_YAW_RIGHT); 1672 inputMap.put(KeyStroke.getKeyStroke("D"), ActionType.ROTATE_CAMERA_YAW_RIGHT); 1673 inputMap.put(KeyStroke.getKeyStroke("shift PAGE_UP"), ActionType.ROTATE_CAMERA_PITCH_FAST_UP); 1674 inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), ActionType.ROTATE_CAMERA_PITCH_UP); 1675 inputMap.put(KeyStroke.getKeyStroke("shift PAGE_DOWN"), ActionType.ROTATE_CAMERA_PITCH_FAST_DOWN); 1676 inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), ActionType.ROTATE_CAMERA_PITCH_DOWN); 1677 inputMap.put(KeyStroke.getKeyStroke("shift HOME"), ActionType.ELEVATE_CAMERA_FAST_UP); 1678 inputMap.put(KeyStroke.getKeyStroke("HOME"), ActionType.ELEVATE_CAMERA_UP); 1679 inputMap.put(KeyStroke.getKeyStroke("shift END"), ActionType.ELEVATE_CAMERA_FAST_DOWN); 1680 inputMap.put(KeyStroke.getKeyStroke("END"), ActionType.ELEVATE_CAMERA_DOWN); 1681 } 1682 1683 /** 1684 * Creates actions that calls back <code>controller</code> methods. 1685 */ createActions(final HomeController3D controller)1686 private void createActions(final HomeController3D controller) { 1687 // Move camera action mapped to arrow keys 1688 class MoveCameraAction extends AbstractAction { 1689 private final float delta; 1690 1691 public MoveCameraAction(float delta) { 1692 this.delta = delta; 1693 } 1694 1695 public void actionPerformed(ActionEvent e) { 1696 controller.moveCamera(this.delta); 1697 } 1698 } 1699 // Move camera sideways action mapped to arrow keys 1700 class MoveCameraSidewaysAction extends AbstractAction { 1701 private final float delta; 1702 1703 public MoveCameraSidewaysAction(float delta) { 1704 this.delta = delta; 1705 } 1706 1707 public void actionPerformed(ActionEvent e) { 1708 controller.moveCameraSideways(this.delta); 1709 } 1710 } 1711 // Elevate camera action mapped to arrow keys 1712 class ElevateCameraAction extends AbstractAction { 1713 private final float delta; 1714 1715 public ElevateCameraAction(float delta) { 1716 this.delta = delta; 1717 } 1718 1719 public void actionPerformed(ActionEvent e) { 1720 controller.elevateCamera(this.delta); 1721 } 1722 } 1723 // Rotate camera yaw action mapped to arrow keys 1724 class RotateCameraYawAction extends AbstractAction { 1725 private final float delta; 1726 1727 public RotateCameraYawAction(float delta) { 1728 this.delta = delta; 1729 } 1730 1731 public void actionPerformed(ActionEvent e) { 1732 controller.rotateCameraYaw(this.delta); 1733 } 1734 } 1735 // Rotate camera pitch action mapped to arrow keys 1736 class RotateCameraPitchAction extends AbstractAction { 1737 private final float delta; 1738 1739 public RotateCameraPitchAction(float delta) { 1740 this.delta = delta; 1741 } 1742 1743 public void actionPerformed(ActionEvent e) { 1744 controller.rotateCameraPitch(this.delta); 1745 } 1746 } 1747 ActionMap actionMap = getActionMap(); 1748 actionMap.put(ActionType.MOVE_CAMERA_FORWARD, new MoveCameraAction(6.5f)); 1749 actionMap.put(ActionType.MOVE_CAMERA_FAST_FORWARD, new MoveCameraAction(32.5f)); 1750 actionMap.put(ActionType.MOVE_CAMERA_BACKWARD, new MoveCameraAction(-6.5f)); 1751 actionMap.put(ActionType.MOVE_CAMERA_FAST_BACKWARD, new MoveCameraAction(-32.5f)); 1752 actionMap.put(ActionType.MOVE_CAMERA_LEFT, new MoveCameraSidewaysAction(-2.5f)); 1753 actionMap.put(ActionType.MOVE_CAMERA_FAST_LEFT, new MoveCameraSidewaysAction(-10f)); 1754 actionMap.put(ActionType.MOVE_CAMERA_RIGHT, new MoveCameraSidewaysAction(2.5f)); 1755 actionMap.put(ActionType.MOVE_CAMERA_FAST_RIGHT, new MoveCameraSidewaysAction(10f)); 1756 actionMap.put(ActionType.ELEVATE_CAMERA_DOWN, new ElevateCameraAction(-2.5f)); 1757 actionMap.put(ActionType.ELEVATE_CAMERA_FAST_DOWN, new ElevateCameraAction(-10f)); 1758 actionMap.put(ActionType.ELEVATE_CAMERA_UP, new ElevateCameraAction(2.5f)); 1759 actionMap.put(ActionType.ELEVATE_CAMERA_FAST_UP, new ElevateCameraAction(10f)); 1760 actionMap.put(ActionType.ROTATE_CAMERA_YAW_LEFT, new RotateCameraYawAction(-(float)Math.PI / 60)); 1761 actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_LEFT, new RotateCameraYawAction(-(float)Math.PI / 12)); 1762 actionMap.put(ActionType.ROTATE_CAMERA_YAW_RIGHT, new RotateCameraYawAction((float)Math.PI / 60)); 1763 actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT, new RotateCameraYawAction((float)Math.PI / 12)); 1764 actionMap.put(ActionType.ROTATE_CAMERA_PITCH_UP, new RotateCameraPitchAction(-(float)Math.PI / 120)); 1765 actionMap.put(ActionType.ROTATE_CAMERA_PITCH_FAST_UP, new RotateCameraPitchAction(-(float)Math.PI / 24)); 1766 actionMap.put(ActionType.ROTATE_CAMERA_PITCH_DOWN, new RotateCameraPitchAction((float)Math.PI / 120)); 1767 actionMap.put(ActionType.ROTATE_CAMERA_PITCH_FAST_DOWN, new RotateCameraPitchAction((float)Math.PI / 24)); 1768 } 1769 1770 @Override addMouseMotionListener(final MouseMotionListener l)1771 public void addMouseMotionListener(final MouseMotionListener l) { 1772 super.addMouseMotionListener(l); 1773 if (this.component3D != null) { 1774 this.component3D.addMouseMotionListener(new MouseMotionListener() { 1775 public void mouseMoved(MouseEvent ev) { 1776 l.mouseMoved(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1777 } 1778 1779 public void mouseDragged(MouseEvent ev) { 1780 l.mouseDragged(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1781 } 1782 }); 1783 } 1784 } 1785 1786 @Override removeMouseMotionListener(final MouseMotionListener l)1787 public void removeMouseMotionListener(final MouseMotionListener l) { 1788 if (this.component3D != null) { 1789 this.component3D.removeMouseMotionListener(l); 1790 } 1791 super.removeMouseMotionListener(l); 1792 } 1793 1794 @Override addMouseListener(final MouseListener l)1795 public void addMouseListener(final MouseListener l) { 1796 super.addMouseListener(l); 1797 if (this.component3D != null) { 1798 this.component3D.addMouseListener(new MouseListener() { 1799 public void mousePressed(MouseEvent ev) { 1800 l.mousePressed(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1801 } 1802 1803 public void mouseClicked(MouseEvent ev) { 1804 l.mouseClicked(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1805 } 1806 1807 public void mouseReleased(MouseEvent ev) { 1808 l.mouseReleased(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1809 } 1810 1811 public void mouseExited(MouseEvent ev) { 1812 l.mouseExited(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1813 } 1814 1815 public void mouseEntered(MouseEvent ev) { 1816 l.mouseEntered(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this)); 1817 } 1818 }); 1819 } 1820 } 1821 1822 @Override removeMouseListener(final MouseListener l)1823 public void removeMouseListener(final MouseListener l) { 1824 if (this.component3D != null) { 1825 this.component3D.removeMouseListener(l); 1826 } 1827 super.removeMouseListener(l); 1828 } 1829 1830 /** 1831 * Returns the closest {@link Selectable} object at component coordinates (x, y), 1832 * or <code>null</code> if not found. 1833 */ getClosestItemAt(int x, int y)1834 public Selectable getClosestItemAt(int x, int y) { 1835 if (this.component3D != null) { 1836 Canvas3D canvas; 1837 if (this.component3D instanceof Canvas3D) { 1838 canvas = (Canvas3D)this.component3D; 1839 } else { 1840 try { 1841 // Call JCanvas3D#getOffscreenCanvas3D by reflection to be able to run under Java 3D 1.3 1842 canvas = (Canvas3D)Class.forName("com.sun.j3d.exp.swing.JCanvas3D").getMethod("getOffscreenCanvas3D").invoke(this.component3D); 1843 } catch (Exception ex) { 1844 UnsupportedOperationException ex2 = new UnsupportedOperationException(); 1845 ex2.initCause(ex); 1846 throw ex2; 1847 } 1848 } 1849 PickCanvas pickCanvas = new PickCanvas(canvas, this.onscreenUniverse.getLocale()); 1850 pickCanvas.setMode(PickCanvas.GEOMETRY); 1851 1852 if (OperatingSystem.isJavaVersionGreaterOrEqual("1.9")) { 1853 try { 1854 // Dirty hack that scales mouse coordinates with xcale and yscale private fields of Canvas3D 1855 Field xscaleField = Canvas3D.class.getDeclaredField("xscale"); 1856 xscaleField.setAccessible(true); 1857 double xscale = (Double)(xscaleField.get(this.component3D)); 1858 Field yscaleField = Canvas3D.class.getDeclaredField("yscale"); 1859 yscaleField.setAccessible(true); 1860 double yscale = (Double)(yscaleField.get(this.component3D)); 1861 x = (int)(x * xscale); 1862 y = (int)(y * yscale); 1863 } catch (Exception ex) { 1864 } 1865 } 1866 1867 Point canvasPoint = SwingUtilities.convertPoint(this, x, y, this.component3D); 1868 pickCanvas.setShapeLocation(canvasPoint.x, canvasPoint.y); 1869 PickResult result = pickCanvas.pickClosest(); 1870 if (result != null) { 1871 Node pickedNode = result.getNode(PickResult.SHAPE3D); 1872 while (!this.homeObjects.containsValue(pickedNode) 1873 && pickedNode.getParent() != null) { 1874 pickedNode = pickedNode.getParent(); 1875 } 1876 if (pickedNode != null) { 1877 for (Map.Entry<Selectable, Object3DBranch> entry : this.homeObjects.entrySet()) { 1878 if (entry.getValue() == pickedNode) { 1879 return entry.getKey(); 1880 } 1881 } 1882 } 1883 } 1884 } 1885 return null; 1886 } 1887 1888 /** 1889 * Returns a new scene tree root. 1890 */ createSceneTree(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)1891 private BranchGroup createSceneTree(boolean displayShadowOnFloor, 1892 boolean listenToHomeUpdates, 1893 boolean waitForLoading) { 1894 BranchGroup root = new BranchGroup(); 1895 // Build scene tree 1896 root.addChild(createHomeTree(displayShadowOnFloor, listenToHomeUpdates, waitForLoading)); 1897 Node backgroundNode = createBackgroundNode(listenToHomeUpdates, waitForLoading); 1898 root.addChild(backgroundNode); 1899 Node groundNode = createGroundNode(-0.5E7f, -0.5E7f, 1E7f, 1E7f, listenToHomeUpdates, waitForLoading); 1900 root.addChild(groundNode); 1901 1902 this.sceneLights = createLights(groundNode, listenToHomeUpdates); 1903 for (Light light : this.sceneLights) { 1904 root.addChild(light); 1905 } 1906 1907 return root; 1908 } 1909 1910 /** 1911 * Returns a new background node. 1912 */ createBackgroundNode(boolean listenToHomeUpdates, final boolean waitForLoading)1913 private Node createBackgroundNode(boolean listenToHomeUpdates, final boolean waitForLoading) { 1914 final Appearance skyBackgroundAppearance = new Appearance(); 1915 ColoringAttributes skyBackgroundColoringAttributes = new ColoringAttributes(); 1916 skyBackgroundAppearance.setColoringAttributes(skyBackgroundColoringAttributes); 1917 TextureAttributes skyBackgroundTextureAttributes = new TextureAttributes(); 1918 skyBackgroundAppearance.setTextureAttributes(skyBackgroundTextureAttributes); 1919 // Allow sky color and texture to change 1920 skyBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE); 1921 skyBackgroundAppearance.setCapability(Appearance.ALLOW_COLORING_ATTRIBUTES_READ); 1922 skyBackgroundColoringAttributes.setCapability(ColoringAttributes.ALLOW_COLOR_WRITE); 1923 skyBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); 1924 skyBackgroundTextureAttributes.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); 1925 1926 Geometry topHalfSphereGeometry = createHalfSphereGeometry(true); 1927 final Shape3D topHalfSphere = new Shape3D(topHalfSphereGeometry, skyBackgroundAppearance); 1928 BranchGroup backgroundBranch = new BranchGroup(); 1929 backgroundBranch.addChild(topHalfSphere); 1930 1931 final Appearance bottomAppearance = new Appearance(); 1932 final RenderingAttributes bottomRenderingAttributes = new RenderingAttributes(); 1933 bottomRenderingAttributes.setVisible(false); 1934 bottomAppearance.setRenderingAttributes(bottomRenderingAttributes); 1935 bottomRenderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE); 1936 Shape3D bottomHalfSphere = new Shape3D(createHalfSphereGeometry(false), bottomAppearance); 1937 backgroundBranch.addChild(bottomHalfSphere); 1938 1939 // Add two planes at ground level to complete landscape at the horizon when camera is above horizon 1940 // (one at y = -0.01 to fill the horizon and a lower one to fill the lower part of the scene) 1941 final Appearance groundBackgroundAppearance = new Appearance(); 1942 TextureAttributes groundBackgroundTextureAttributes = new TextureAttributes(); 1943 groundBackgroundTextureAttributes.setTextureMode(TextureAttributes.MODULATE); 1944 groundBackgroundAppearance.setTextureAttributes(groundBackgroundTextureAttributes); 1945 groundBackgroundAppearance.setTexCoordGeneration( 1946 new TexCoordGeneration(TexCoordGeneration.OBJECT_LINEAR, TexCoordGeneration.TEXTURE_COORDINATE_2, 1947 new Vector4f(1E5f, 0, 0, 0), new Vector4f(0, 0, 1E5f, 0))); 1948 final RenderingAttributes groundRenderingAttributes = new RenderingAttributes(); 1949 groundBackgroundAppearance.setRenderingAttributes(groundRenderingAttributes); 1950 // Allow ground color and texture to change 1951 groundBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE); 1952 groundBackgroundAppearance.setCapability(Appearance.ALLOW_MATERIAL_WRITE); 1953 groundRenderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE); 1954 1955 GeometryInfo geometryInfo = new GeometryInfo (GeometryInfo.QUAD_ARRAY); 1956 geometryInfo.setCoordinates(new Point3f [] { 1957 new Point3f(-1f, -0.01f, -1f), 1958 new Point3f(-1f, -0.01f, 1f), 1959 new Point3f(1f, -0.01f, 1f), 1960 new Point3f(1f, -0.01f, -1f), 1961 new Point3f(-1f, -0.1f, -1f), 1962 new Point3f(-1f, -0.1f, 1f), 1963 new Point3f(1f, -0.1f, 1f), 1964 new Point3f(1f, -0.1f, -1f)}); 1965 geometryInfo.setCoordinateIndices(new int [] {0, 1, 2, 3, 4, 5, 6, 7}); 1966 geometryInfo.setNormals(new Vector3f [] {new Vector3f(0, 1, 0)}); 1967 geometryInfo.setNormalIndices(new int [] {0, 0, 0, 0, 0, 0, 0, 0}); 1968 Shape3D groundBackground = new Shape3D(geometryInfo.getIndexedGeometryArray(), groundBackgroundAppearance); 1969 backgroundBranch.addChild(groundBackground); 1970 1971 // Add its own lights to background to ensure they have an effect 1972 for (Light light : createBackgroundLights(listenToHomeUpdates)) { 1973 backgroundBranch.addChild(light); 1974 } 1975 1976 final Background background = new Background(backgroundBranch); 1977 updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, this.home, waitForLoading); 1978 background.setApplicationBounds(new BoundingBox( 1979 new Point3d(-1E7, -1E7, -1E7), 1980 new Point3d(1E7, 1E7, 1E7))); 1981 1982 if (listenToHomeUpdates) { 1983 // Add a listener on sky color and texture properties change 1984 this.backgroundChangeListener = new PropertyChangeListener() { 1985 public void propertyChange(PropertyChangeEvent ev) { 1986 updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, home, waitForLoading); 1987 } 1988 }; 1989 this.home.getEnvironment().addPropertyChangeListener( 1990 HomeEnvironment.Property.SKY_COLOR, this.backgroundChangeListener); 1991 this.home.getEnvironment().addPropertyChangeListener( 1992 HomeEnvironment.Property.SKY_TEXTURE, this.backgroundChangeListener); 1993 this.home.getEnvironment().addPropertyChangeListener( 1994 HomeEnvironment.Property.GROUND_COLOR, this.backgroundChangeListener); 1995 this.home.getEnvironment().addPropertyChangeListener( 1996 HomeEnvironment.Property.GROUND_TEXTURE, this.backgroundChangeListener); 1997 // Make groundBackground invisible and bottom half sphere visible if camera is below the ground 1998 this.elevationChangeListener = new PropertyChangeListener() { 1999 public void propertyChange(PropertyChangeEvent ev) { 2000 if (ev.getSource() == home) { 2001 // Move listener to the new camera 2002 ((Camera)ev.getOldValue()).removePropertyChangeListener(this); 2003 home.getCamera().addPropertyChangeListener(this); 2004 } 2005 if (ev.getSource() == home 2006 || Camera.Property.Z.name().equals(ev.getPropertyName())) { 2007 groundRenderingAttributes.setVisible(home.getCamera().getZ() >= 0); 2008 bottomRenderingAttributes.setVisible(home.getCamera().getZ() < 0); 2009 } 2010 } 2011 }; 2012 this.home.getCamera().addPropertyChangeListener(this.elevationChangeListener); 2013 this.home.addPropertyChangeListener(Home.Property.CAMERA, this.elevationChangeListener); 2014 } 2015 return background; 2016 } 2017 2018 /** 2019 * Returns a half sphere oriented inward and with texture ordinates 2020 * that spread along an hemisphere. 2021 */ createHalfSphereGeometry(boolean top)2022 private Geometry createHalfSphereGeometry(boolean top) { 2023 final int divisionCount = 48; 2024 Point3f [] coords = new Point3f [divisionCount * divisionCount]; 2025 TexCoord2f [] textureCoords = top ? new TexCoord2f [divisionCount * divisionCount] : null; 2026 Color3f [] colors = top ? null : new Color3f [divisionCount * divisionCount]; 2027 for (int i = 0, k = 0; i < divisionCount; i++) { 2028 double alpha = i * 2 * Math.PI / divisionCount; 2029 float cosAlpha = (float)Math.cos(alpha); 2030 float sinAlpha = (float)Math.sin(alpha); 2031 double nextAlpha = (i + 1) * 2 * Math.PI / divisionCount; 2032 float cosNextAlpha = (float)Math.cos(nextAlpha); 2033 float sinNextAlpha = (float)Math.sin(nextAlpha); 2034 for (int j = 0, max = divisionCount / 4; j < max; j++) { 2035 double beta = 2 * j * Math.PI / divisionCount; 2036 float cosBeta = (float)Math.cos(beta); 2037 float sinBeta = (float)Math.sin(beta); 2038 // Correct the bottom of the hemisphere to avoid seeing a bottom hemisphere at the horizon 2039 float y = j != 0 ? (top ? sinBeta : -sinBeta) : -0.01f; 2040 double nextBeta = 2 * (j + 1) * Math.PI / divisionCount; 2041 if (!top) { 2042 nextBeta = -nextBeta; 2043 } 2044 float cosNextBeta = (float)Math.cos(nextBeta); 2045 float sinNextBeta = (float)Math.sin(nextBeta); 2046 if (top) { 2047 coords [k] = new Point3f(cosAlpha * cosBeta, y, sinAlpha * cosBeta); 2048 textureCoords [k++] = new TexCoord2f((float)i / divisionCount, (float)j / max); 2049 2050 coords [k] = new Point3f(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta); 2051 textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, (float)j / max); 2052 2053 coords [k] = new Point3f(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta); 2054 textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, (float)(j + 1) / max); 2055 2056 coords [k] = new Point3f(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta); 2057 textureCoords [k++] = new TexCoord2f((float)i / divisionCount, (float)(j + 1) / max); 2058 } else { 2059 coords [k] = new Point3f(cosAlpha * cosBeta, y, sinAlpha * cosBeta); 2060 float color1 = .9f + y * .5f; 2061 colors [k++] = new Color3f(color1, color1, color1); 2062 2063 coords [k] = new Point3f(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta); 2064 float color2 = .9f + sinNextBeta * .5f; 2065 colors [k++] = new Color3f(color2, color2, color2); 2066 2067 coords [k] = new Point3f(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta); 2068 colors [k++] = new Color3f(color2, color2, color2); 2069 2070 coords [k] = new Point3f(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta); 2071 colors [k++] = new Color3f(color1, color1, color1); 2072 } 2073 } 2074 } 2075 2076 GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.QUAD_ARRAY); 2077 geometryInfo.setCoordinates(coords); 2078 if (textureCoords != null) { 2079 geometryInfo.setTextureCoordinateParams(1, 2); 2080 geometryInfo.setTextureCoordinates(0, textureCoords); 2081 } 2082 if (colors != null) { 2083 geometryInfo.setColors(colors); 2084 } 2085 geometryInfo.indexify(); 2086 geometryInfo.compact(); 2087 Geometry halfSphereGeometry = geometryInfo.getIndexedGeometryArray(); 2088 return halfSphereGeometry; 2089 } 2090 2091 /** 2092 * Updates <code>backgroundAppearance</code> color and texture from <code>home</code> sky color and texture. 2093 */ updateBackgroundColorAndTexture(final Appearance skyBackgroundAppearance, final Appearance groundBackgroundAppearance, Home home, boolean waitForLoading)2094 private void updateBackgroundColorAndTexture(final Appearance skyBackgroundAppearance, 2095 final Appearance groundBackgroundAppearance, 2096 Home home, 2097 boolean waitForLoading) { 2098 Color3f skyColor = new Color3f(new Color(home.getEnvironment().getSkyColor())); 2099 skyBackgroundAppearance.getColoringAttributes().setColor(skyColor); 2100 HomeTexture skyTexture = home.getEnvironment().getSkyTexture(); 2101 if (skyTexture != null) { 2102 final Transform3D transform = new Transform3D(); 2103 transform.setTranslation(new Vector3f(-skyTexture.getXOffset(), 0, 0)); 2104 TextureManager textureManager = TextureManager.getInstance(); 2105 if (waitForLoading) { 2106 // Don't share the background texture otherwise if might not be rendered correctly 2107 skyBackgroundAppearance.setTexture(textureManager.loadTexture(skyTexture.getImage())); 2108 skyBackgroundAppearance.getTextureAttributes().setTextureTransform(transform); 2109 } else { 2110 textureManager.loadTexture(skyTexture.getImage(), waitForLoading, 2111 new TextureManager.TextureObserver() { 2112 public void textureUpdated(Texture texture) { 2113 // Use a copy of the texture in case it's used in an other universe 2114 skyBackgroundAppearance.setTexture((Texture)texture.cloneNodeComponent(false)); 2115 skyBackgroundAppearance.getTextureAttributes().setTextureTransform(transform); 2116 } 2117 }); 2118 } 2119 } else { 2120 skyBackgroundAppearance.setTexture(null); 2121 } 2122 2123 HomeTexture groundTexture = home.getEnvironment().getGroundTexture(); 2124 if (groundTexture != null) { 2125 groundBackgroundAppearance.setMaterial(new Material( 2126 new Color3f(1, 1, 1), new Color3f(), new Color3f(1, 1, 1), new Color3f(0, 0, 0), 1)); 2127 TextureManager textureManager = TextureManager.getInstance(); 2128 if (waitForLoading) { 2129 groundBackgroundAppearance.setTexture(textureManager.loadTexture(groundTexture.getImage())); 2130 } else { 2131 textureManager.loadTexture(groundTexture.getImage(), waitForLoading, 2132 new TextureManager.TextureObserver() { 2133 public void textureUpdated(Texture texture) { 2134 // Use a copy of the texture in case it's used in an other universe 2135 groundBackgroundAppearance.setTexture((Texture)texture.cloneNodeComponent(false)); 2136 } 2137 }); 2138 } 2139 } else { 2140 int groundColor = home.getEnvironment().getGroundColor(); 2141 Color3f color = new Color3f(((groundColor >>> 16) & 0xFF) / 255.f, 2142 ((groundColor >>> 8) & 0xFF) / 255.f, 2143 (groundColor & 0xFF) / 255.f); 2144 groundBackgroundAppearance.setMaterial(new Material(color, new Color3f(), color, new Color3f(0, 0, 0), 1)); 2145 groundBackgroundAppearance.setTexture(null); 2146 } 2147 2148 clearPrintedImageCache(); 2149 } 2150 2151 /** 2152 * Returns a new ground node. 2153 */ createGroundNode(final float groundOriginX, final float groundOriginY, final float groundWidth, final float groundDepth, boolean listenToHomeUpdates, boolean waitForLoading)2154 private Node createGroundNode(final float groundOriginX, 2155 final float groundOriginY, 2156 final float groundWidth, 2157 final float groundDepth, 2158 boolean listenToHomeUpdates, 2159 boolean waitForLoading) { 2160 final Ground3D ground3D = new Ground3D(this.home, 2161 groundOriginX, groundOriginY, groundWidth, groundDepth, waitForLoading); 2162 Transform3D translation = new Transform3D(); 2163 translation.setTranslation(new Vector3f(0, -0.2f, 0)); 2164 TransformGroup transformGroup = new TransformGroup(translation); 2165 transformGroup.addChild(ground3D); 2166 2167 if (listenToHomeUpdates) { 2168 // Add a listener on ground color and texture properties change 2169 this.groundChangeListener = new PropertyChangeListener() { 2170 private Runnable updater; 2171 public void propertyChange(PropertyChangeEvent ev) { 2172 if (this.updater == null) { 2173 // Group updates 2174 EventQueue.invokeLater(this.updater = new Runnable () { 2175 public void run() { 2176 ground3D.update(); 2177 updater = null; 2178 } 2179 }); 2180 } 2181 clearPrintedImageCache(); 2182 } 2183 }; 2184 HomeEnvironment homeEnvironment = this.home.getEnvironment(); 2185 homeEnvironment.addPropertyChangeListener( 2186 HomeEnvironment.Property.GROUND_COLOR, this.groundChangeListener); 2187 homeEnvironment.addPropertyChangeListener( 2188 HomeEnvironment.Property.GROUND_TEXTURE, this.groundChangeListener); 2189 homeEnvironment.addPropertyChangeListener( 2190 HomeEnvironment.Property.BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D, this.groundChangeListener); 2191 this.home.addPropertyChangeListener(Home.Property.BACKGROUND_IMAGE, this.groundChangeListener); 2192 } 2193 2194 return transformGroup; 2195 } 2196 2197 /** 2198 * Returns the lights used for the background. 2199 */ createBackgroundLights(boolean listenToHomeUpdates)2200 private Light [] createBackgroundLights(boolean listenToHomeUpdates) { 2201 final Light [] lights = { 2202 // Use just one direct light for background because only one horizontal plane is under light 2203 new DirectionalLight(new Color3f(1.435f, 1.435f, 1.435f), new Vector3f(0f, -1f, 0f)), 2204 new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))}; 2205 for (int i = 0; i < lights.length - 1; i++) { 2206 // Allow directional lights color and influencing bounds to change 2207 lights [i].setCapability(DirectionalLight.ALLOW_COLOR_WRITE); 2208 // Store default color in user data 2209 Color3f defaultColor = new Color3f(); 2210 lights [i].getColor(defaultColor); 2211 lights [i].setUserData(defaultColor); 2212 updateLightColor(lights [i]); 2213 } 2214 2215 final Bounds defaultInfluencingBounds = new BoundingSphere(new Point3d(), 2); 2216 for (Light light : lights) { 2217 light.setInfluencingBounds(defaultInfluencingBounds); 2218 } 2219 2220 if (listenToHomeUpdates) { 2221 // Add a listener on light color property change to home 2222 this.backgroundLightColorListener = new PropertyChangeListener() { 2223 public void propertyChange(PropertyChangeEvent ev) { 2224 updateLightColor(lights [0]); 2225 } 2226 }; 2227 this.home.getEnvironment().addPropertyChangeListener( 2228 HomeEnvironment.Property.LIGHT_COLOR, this.backgroundLightColorListener); 2229 } 2230 2231 return lights; 2232 } 2233 2234 /** 2235 * Returns the lights of the scene. 2236 */ createLights(final Node groundNode, boolean listenToHomeUpdates)2237 private Light [] createLights(final Node groundNode, boolean listenToHomeUpdates) { 2238 final Light [] lights = { 2239 new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(1.5f, -0.8f, -1)), 2240 new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(-1.5f, -0.8f, -1)), 2241 new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(0, -0.8f, 1)), 2242 new DirectionalLight(new Color3f(0.7f, 0.7f, 0.7f), new Vector3f(0, 1f, 0)), 2243 new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))}; 2244 for (int i = 0; i < lights.length - 1; i++) { 2245 // Allow directional lights color and influencing bounds to change 2246 lights [i].setCapability(DirectionalLight.ALLOW_COLOR_WRITE); 2247 lights [i].setCapability(DirectionalLight.ALLOW_SCOPE_WRITE); 2248 // Store default color in user data 2249 Color3f defaultColor = new Color3f(); 2250 lights [i].getColor(defaultColor); 2251 lights [i].setUserData(defaultColor); 2252 updateLightColor(lights [i]); 2253 } 2254 2255 final Bounds defaultInfluencingBounds = new BoundingSphere(new Point3d(), 1E7); 2256 for (Light light : lights) { 2257 light.setInfluencingBounds(defaultInfluencingBounds); 2258 } 2259 2260 if (listenToHomeUpdates) { 2261 // Add a listener on light color property change to home 2262 this.lightColorListener = new PropertyChangeListener() { 2263 public void propertyChange(PropertyChangeEvent ev) { 2264 for (int i = 0; i < lights.length - 1; i++) { 2265 updateLightColor(lights [i]); 2266 } 2267 updateObjects(getHomeObjects(HomeLight.class)); 2268 } 2269 }; 2270 this.home.getEnvironment().addPropertyChangeListener( 2271 HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener); 2272 2273 // Add a listener on subpart size property change to home 2274 this.subpartSizeListener = new PropertyChangeListener() { 2275 public void propertyChange(PropertyChangeEvent ev) { 2276 if (ev != null) { 2277 // Update 3D objects if not at initialization 2278 Collection<Selectable> homeItems = new ArrayList<Selectable>(home.getWalls()); 2279 homeItems.addAll(home.getRooms()); 2280 homeItems.addAll(getHomeObjects(HomeLight.class)); 2281 updateObjects(homeItems); 2282 clearPrintedImageCache(); 2283 } 2284 2285 // Update default lights scope 2286 List<Group> scope = null; 2287 if (home.getEnvironment().getSubpartSizeUnderLight() > 0) { 2288 Area lightScopeOutsideWallsArea = getLightScopeOutsideWallsArea(); 2289 scope = new ArrayList<Group>(); 2290 for (Wall wall : home.getWalls()) { 2291 Object3DBranch wall3D = homeObjects.get(wall); 2292 if (wall3D instanceof Wall3D) { 2293 // Add left and/or right side of the wall to scope 2294 float [][] points = wall.getPoints(); 2295 if (!lightScopeOutsideWallsArea.contains(points [0][0], points [0][1])) { 2296 scope.add((Group)wall3D.getChild(1)); 2297 } 2298 if (!lightScopeOutsideWallsArea.contains(points [points.length - 1][0], points [points.length - 1][1])) { 2299 scope.add((Group)wall3D.getChild(4)); 2300 } 2301 } 2302 // Add wall top and bottom groups to scope 2303 scope.add((Group)wall3D.getChild(0)); 2304 scope.add((Group)wall3D.getChild(2)); 2305 scope.add((Group)wall3D.getChild(3)); 2306 scope.add((Group)wall3D.getChild(5)); 2307 } 2308 List<Selectable> otherItems = new ArrayList<Selectable>(home.getRooms()); 2309 otherItems.addAll(getHomeObjects(HomePieceOfFurniture.class)); 2310 for (Selectable item : otherItems) { 2311 // Add item to scope if one of its points don't belong to lightScopeWallsArea 2312 for (float [] point : item.getPoints()) { 2313 if (!lightScopeOutsideWallsArea.contains(point [0], point [1])) { 2314 Group object3D = homeObjects.get(item); 2315 if (object3D instanceof HomePieceOfFurniture3D) { 2316 // Add the direct parent of the shape that will be added once loaded 2317 // otherwise scope won't be updated automatically 2318 object3D = (Group)object3D.getChild(0); 2319 } 2320 scope.add(object3D); 2321 break; 2322 } 2323 } 2324 } 2325 } else { 2326 lightScopeOutsideWallsAreaCache = null; 2327 } 2328 2329 for (Light light : lights) { 2330 if (light instanceof DirectionalLight) { 2331 light.removeAllScopes(); 2332 if (scope != null) { 2333 light.addScope((Group)groundNode); 2334 for (Group group : scope) { 2335 light.addScope(group); 2336 } 2337 } 2338 } 2339 } 2340 } 2341 }; 2342 2343 this.home.getEnvironment().addPropertyChangeListener( 2344 HomeEnvironment.Property.SUBPART_SIZE_UNDER_LIGHT, this.subpartSizeListener); 2345 this.subpartSizeListener.propertyChange(null); 2346 } 2347 2348 return lights; 2349 } 2350 2351 /** 2352 * Returns the home objects displayed by this component of the given class. 2353 */ getHomeObjects(Class<T> objectClass)2354 private <T> List<T> getHomeObjects(Class<T> objectClass) { 2355 return Home.getSubList(new ArrayList<Selectable>(homeObjects.keySet()), objectClass); 2356 } 2357 2358 /** 2359 * Updates<code>light</code> color from <code>home</code> light color. 2360 */ updateLightColor(Light light)2361 private void updateLightColor(Light light) { 2362 Color3f defaultColor = (Color3f)light.getUserData(); 2363 int lightColor = this.home.getEnvironment().getLightColor(); 2364 light.setColor(new Color3f(((lightColor >>> 16) & 0xFF) / 255f * defaultColor.x, 2365 ((lightColor >>> 8) & 0xFF) / 255f * defaultColor.y, 2366 (lightColor & 0xFF) / 255f * defaultColor.z)); 2367 clearPrintedImageCache(); 2368 } 2369 2370 /** 2371 * Returns walls area used for light scope outside. 2372 */ getLightScopeOutsideWallsArea()2373 private Area getLightScopeOutsideWallsArea() { 2374 if (this.lightScopeOutsideWallsAreaCache == null) { 2375 // Compute a smaller area surrounding all walls at all levels 2376 Area wallsPath = new Area(); 2377 for (Wall wall : this.home.getWalls()) { 2378 Wall thinnerWall = wall.clone(); 2379 thinnerWall.setThickness(Math.max(thinnerWall.getThickness() - 0.1f, 0.08f)); 2380 wallsPath.add(new Area(getShape(thinnerWall.getPoints()))); 2381 } 2382 Area lightScopeOutsideWallsArea = new Area(); 2383 List<float []> points = new ArrayList<float[]>(); 2384 for (PathIterator it = wallsPath.getPathIterator(null, 1); !it.isDone(); it.next()) { 2385 float [] point = new float[2]; 2386 switch (it.currentSegment(point)) { 2387 case PathIterator.SEG_MOVETO : 2388 case PathIterator.SEG_LINETO : 2389 points.add(point); 2390 break; 2391 case PathIterator.SEG_CLOSE : 2392 if (points.size() > 2) { 2393 float [][] pointsArray = points.toArray(new float [points.size()][]); 2394 if (new Room(pointsArray).isClockwise()) { 2395 lightScopeOutsideWallsArea.add(new Area(getShape(pointsArray))); 2396 } 2397 } 2398 points.clear(); 2399 break; 2400 } 2401 } 2402 this.lightScopeOutsideWallsAreaCache = lightScopeOutsideWallsArea; 2403 } 2404 return this.lightScopeOutsideWallsAreaCache; 2405 } 2406 2407 /** 2408 * Returns a <code>home</code> new tree node, with branches for each wall 2409 * and piece of furniture of <code>home</code>. 2410 */ createHomeTree(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)2411 private Node createHomeTree(boolean displayShadowOnFloor, 2412 boolean listenToHomeUpdates, 2413 boolean waitForLoading) { 2414 Group homeRoot = createHomeRoot(); 2415 // Add walls, pieces, rooms, polylines and labels already available 2416 for (Label label : this.home.getLabels()) { 2417 addObject(homeRoot, label, listenToHomeUpdates, waitForLoading); 2418 } 2419 for (Polyline polyline : this.home.getPolylines()) { 2420 addObject(homeRoot, polyline, listenToHomeUpdates, waitForLoading); 2421 } 2422 for (Room room : this.home.getRooms()) { 2423 addObject(homeRoot, room, listenToHomeUpdates, waitForLoading); 2424 } 2425 for (Wall wall : this.home.getWalls()) { 2426 addObject(homeRoot, wall, listenToHomeUpdates, waitForLoading); 2427 } 2428 Map<HomePieceOfFurniture, Node> pieces3D = new HashMap<HomePieceOfFurniture, Node>(); 2429 for (HomePieceOfFurniture piece : this.home.getFurniture()) { 2430 if (piece instanceof HomeFurnitureGroup) { 2431 for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) { 2432 if (!(childPiece instanceof HomeFurnitureGroup)) { 2433 pieces3D.put(childPiece, addObject(homeRoot, childPiece, listenToHomeUpdates, waitForLoading)); 2434 } 2435 } 2436 } else { 2437 pieces3D.put(piece, addObject(homeRoot, piece, listenToHomeUpdates, waitForLoading)); 2438 } 2439 } 2440 2441 if (displayShadowOnFloor) { 2442 addShadowOnFloor(homeRoot, pieces3D); 2443 } 2444 2445 if (listenToHomeUpdates) { 2446 // Add level, wall, furniture, room listeners to home for further update 2447 addLevelListener(homeRoot); 2448 addWallListener(homeRoot); 2449 addFurnitureListener(homeRoot); 2450 addRoomListener(homeRoot); 2451 addPolylineListener(homeRoot); 2452 addLabelListener(homeRoot); 2453 // Add environment listeners 2454 addEnvironmentListeners(); 2455 // Should update shadow on floor too but in the facts 2456 // User Interface doesn't propose to modify the furniture of a home 2457 // that displays shadow on floor yet 2458 } 2459 return homeRoot; 2460 } 2461 2462 /** 2463 * Returns a new group at home subtree root. 2464 */ createHomeRoot()2465 private Group createHomeRoot() { 2466 Group homeGroup = new Group(); 2467 // Allow group to have new children 2468 homeGroup.setCapability(Group.ALLOW_CHILDREN_WRITE); 2469 homeGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND); 2470 return homeGroup; 2471 } 2472 2473 /** 2474 * Adds a level listener to home levels that updates the children of the given 2475 * <code>group</code>, each time a level is added, updated or deleted. 2476 */ addLevelListener(final Group group)2477 private void addLevelListener(final Group group) { 2478 this.levelChangeListener = new PropertyChangeListener() { 2479 public void propertyChange(PropertyChangeEvent ev) { 2480 if (Level.Property.VISIBLE.name().equals(ev.getPropertyName()) 2481 || Level.Property.VIEWABLE.name().equals(ev.getPropertyName())) { 2482 Set<Selectable> objects = homeObjects.keySet(); 2483 ArrayList<Selectable> updatedItems = new ArrayList<Selectable>(objects.size()); 2484 for (Selectable item : objects) { 2485 if (item instanceof Room // 3D rooms depend on rooms at other levels 2486 || !(item instanceof Elevatable) 2487 || ((Elevatable)item).isAtLevel((Level)ev.getSource())) { 2488 updatedItems.add(item); 2489 } 2490 } 2491 updateObjects(updatedItems); 2492 groundChangeListener.propertyChange(null); 2493 } else if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())) { 2494 updateObjects(homeObjects.keySet()); 2495 groundChangeListener.propertyChange(null); 2496 } else if (Level.Property.BACKGROUND_IMAGE.name().equals(ev.getPropertyName())) { 2497 groundChangeListener.propertyChange(null); 2498 } else if (Level.Property.FLOOR_THICKNESS.name().equals(ev.getPropertyName())) { 2499 updateObjects(home.getWalls()); 2500 updateObjects(home.getRooms()); 2501 } else if (Level.Property.HEIGHT.name().equals(ev.getPropertyName())) { 2502 updateObjects(home.getRooms()); 2503 } 2504 } 2505 }; 2506 for (Level level : this.home.getLevels()) { 2507 level.addPropertyChangeListener(this.levelChangeListener); 2508 } 2509 this.levelListener = new CollectionListener<Level>() { 2510 public void collectionChanged(CollectionEvent<Level> ev) { 2511 Level level = ev.getItem(); 2512 switch (ev.getType()) { 2513 case ADD : 2514 level.addPropertyChangeListener(levelChangeListener); 2515 break; 2516 case DELETE : 2517 level.removePropertyChangeListener(levelChangeListener); 2518 break; 2519 } 2520 updateObjects(home.getRooms()); 2521 } 2522 }; 2523 this.home.addLevelsListener(this.levelListener); 2524 } 2525 2526 /** 2527 * Adds a wall listener to home walls that updates the children of the given 2528 * <code>group</code>, each time a wall is added, updated or deleted. 2529 */ addWallListener(final Group group)2530 private void addWallListener(final Group group) { 2531 this.wallChangeListener = new PropertyChangeListener() { 2532 public void propertyChange(PropertyChangeEvent ev) { 2533 String propertyName = ev.getPropertyName(); 2534 if (!Wall.Property.PATTERN.name().equals(propertyName)) { 2535 Wall updatedWall = (Wall)ev.getSource(); 2536 updateWall(updatedWall); 2537 List<Level> levels = home.getLevels(); 2538 if (updatedWall.getLevel() == null 2539 || updatedWall.isAtLevel(levels.get(levels.size() - 1))) { 2540 // Update rooms which ceiling height may need an update at last level 2541 updateObjects(home.getRooms()); 2542 } 2543 if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) { 2544 groundChangeListener.propertyChange(null); 2545 } 2546 if (home.getEnvironment().getSubpartSizeUnderLight() > 0) { 2547 if (Wall.Property.X_START.name().equals(propertyName) 2548 || Wall.Property.Y_START.name().equals(propertyName) 2549 || Wall.Property.X_END.name().equals(propertyName) 2550 || Wall.Property.Y_END.name().equals(propertyName) 2551 || Wall.Property.ARC_EXTENT.name().equals(propertyName) 2552 || Wall.Property.THICKNESS.name().equals(propertyName)) { 2553 lightScopeOutsideWallsAreaCache = null; 2554 updateObjectsLightScope(null); 2555 } 2556 } 2557 } 2558 } 2559 }; 2560 for (Wall wall : this.home.getWalls()) { 2561 wall.addPropertyChangeListener(this.wallChangeListener); 2562 } 2563 this.wallListener = new CollectionListener<Wall>() { 2564 public void collectionChanged(CollectionEvent<Wall> ev) { 2565 Wall wall = ev.getItem(); 2566 switch (ev.getType()) { 2567 case ADD : 2568 addObject(group, wall, true, false); 2569 wall.addPropertyChangeListener(wallChangeListener); 2570 break; 2571 case DELETE : 2572 deleteObject(wall); 2573 wall.removePropertyChangeListener(wallChangeListener); 2574 break; 2575 } 2576 lightScopeOutsideWallsAreaCache = null; 2577 updateObjects(home.getRooms()); 2578 groundChangeListener.propertyChange(null); 2579 updateObjectsLightScope(null); 2580 } 2581 }; 2582 this.home.addWallsListener(this.wallListener); 2583 } 2584 2585 /** 2586 * Adds a furniture listener to home that updates the children of the given <code>group</code>, 2587 * each time a piece of furniture is added, updated or deleted. 2588 */ addFurnitureListener(final Group group)2589 private void addFurnitureListener(final Group group) { 2590 this.furnitureChangeListener = new PropertyChangeListener() { 2591 public void propertyChange(PropertyChangeEvent ev) { 2592 HomePieceOfFurniture updatedPiece = (HomePieceOfFurniture)ev.getSource(); 2593 String propertyName = ev.getPropertyName(); 2594 if (HomePieceOfFurniture.Property.X.name().equals(propertyName) 2595 || HomePieceOfFurniture.Property.Y.name().equals(propertyName) 2596 || HomePieceOfFurniture.Property.ANGLE.name().equals(propertyName) 2597 || HomePieceOfFurniture.Property.ROLL.name().equals(propertyName) 2598 || HomePieceOfFurniture.Property.PITCH.name().equals(propertyName) 2599 || HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName) 2600 || HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)) { 2601 updatePieceOfFurnitureGeometry(updatedPiece, propertyName, (Float)ev.getOldValue()); 2602 updateObjectsLightScope(Arrays.asList(new HomePieceOfFurniture [] {updatedPiece})); 2603 } else if (HomePieceOfFurniture.Property.HEIGHT.name().equals(propertyName) 2604 || HomePieceOfFurniture.Property.ELEVATION.name().equals(propertyName) 2605 || HomePieceOfFurniture.Property.MODEL.name().equals(propertyName) 2606 || HomePieceOfFurniture.Property.MODEL_ROTATION.name().equals(propertyName) 2607 || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(propertyName) 2608 || HomePieceOfFurniture.Property.BACK_FACE_SHOWN.name().equals(propertyName) 2609 || HomePieceOfFurniture.Property.MODEL_TRANSFORMATIONS.name().equals(propertyName) 2610 || HomePieceOfFurniture.Property.STAIRCASE_CUT_OUT_SHAPE.name().equals(propertyName) 2611 || HomePieceOfFurniture.Property.VISIBLE.name().equals(propertyName) 2612 || HomePieceOfFurniture.Property.LEVEL.name().equals(propertyName)) { 2613 updatePieceOfFurnitureGeometry(updatedPiece, null, null); 2614 } else if (HomeDoorOrWindow.Property.CUT_OUT_SHAPE.name().equals(propertyName) 2615 || HomeDoorOrWindow.Property.WALL_CUT_OUT_ON_BOTH_SIDES.name().equals(propertyName) 2616 || HomeDoorOrWindow.Property.WALL_WIDTH.name().equals(propertyName) 2617 || HomeDoorOrWindow.Property.WALL_LEFT.name().equals(propertyName) 2618 || HomeDoorOrWindow.Property.WALL_HEIGHT.name().equals(propertyName) 2619 || HomeDoorOrWindow.Property.WALL_TOP.name().equals(propertyName)) { 2620 if (containsDoorsAndWindows(updatedPiece)) { 2621 updateIntersectingWalls(updatedPiece); 2622 } 2623 } else if (HomePieceOfFurniture.Property.COLOR.name().equals(propertyName) 2624 || HomePieceOfFurniture.Property.TEXTURE.name().equals(propertyName) 2625 || HomePieceOfFurniture.Property.MODEL_MATERIALS.name().equals(propertyName) 2626 || HomePieceOfFurniture.Property.SHININESS.name().equals(propertyName) 2627 || (HomeLight.Property.POWER.name().equals(propertyName) 2628 && home.getEnvironment().getSubpartSizeUnderLight() > 0)) { 2629 updateObjects(Arrays.asList(new HomePieceOfFurniture [] {updatedPiece})); 2630 } 2631 } 2632 2633 private void updatePieceOfFurnitureGeometry(HomePieceOfFurniture piece, String propertyName, Float oldValue) { 2634 updateObjects(Arrays.asList(new HomePieceOfFurniture [] {piece})); 2635 if (containsDoorsAndWindows(piece)) { 2636 if (oldValue != null) { 2637 HomePieceOfFurniture oldPiece = piece.clone(); 2638 // Reset the modified property to its old value 2639 if (HomePieceOfFurniture.Property.X.name().equals(propertyName)) { 2640 oldPiece.setX(oldValue); 2641 } else if (HomePieceOfFurniture.Property.Y.name().equals(propertyName)) { 2642 oldPiece.setY(oldValue); 2643 } else if (HomePieceOfFurniture.Property.ANGLE.name().equals(propertyName)) { 2644 oldPiece.setAngle(oldValue); 2645 } else if (HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)) { 2646 oldPiece.setWidth(oldValue); 2647 } else if (HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)) { 2648 oldPiece.setDepth(oldValue); 2649 } 2650 // For doors and windows, propertyName can't be equal to ROLL or PITCH 2651 2652 // Update walls which intersect the piece with its old property value and the one with the new value 2653 updateIntersectingWalls(oldPiece, piece); 2654 } else { 2655 // Property value change won't influence the walls that intersect the door or window 2656 updateIntersectingWalls(piece); 2657 } 2658 } else if (containsStaircases(piece)) { 2659 updateObjects(home.getRooms()); 2660 } 2661 if (piece.getLevel() != null && piece.getLevel().getElevation() < 0) { 2662 groundChangeListener.propertyChange(null); 2663 } 2664 } 2665 }; 2666 for (HomePieceOfFurniture piece : this.home.getFurniture()) { 2667 addPropertyChangeListener(piece, this.furnitureChangeListener); 2668 } 2669 this.furnitureListener = new CollectionListener<HomePieceOfFurniture>() { 2670 public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) { 2671 HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getItem(); 2672 switch (ev.getType()) { 2673 case ADD : 2674 addPieceOfFurniture(group, piece, true, false); 2675 addPropertyChangeListener(piece, furnitureChangeListener); 2676 break; 2677 case DELETE : 2678 deletePieceOfFurniture(piece); 2679 removePropertyChangeListener(piece, furnitureChangeListener); 2680 break; 2681 } 2682 // If piece is or contains a door or a window, update walls that intersect with piece 2683 if (containsDoorsAndWindows(piece)) { 2684 updateIntersectingWalls(piece); 2685 } else if (containsStaircases(piece)) { 2686 updateObjects(home.getRooms()); 2687 } else { 2688 approximateHomeBoundsCache = null; 2689 } 2690 groundChangeListener.propertyChange(null); 2691 updateObjectsLightScope(Arrays.asList(new HomePieceOfFurniture [] {piece})); 2692 } 2693 }; 2694 this.home.addFurnitureListener(this.furnitureListener); 2695 } 2696 2697 /** 2698 * Adds the given <code>listener</code> to <code>piece</code> and its children. 2699 */ addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)2700 private void addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) { 2701 if (piece instanceof HomeFurnitureGroup) { 2702 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 2703 addPropertyChangeListener(child, listener); 2704 } 2705 } else { 2706 piece.addPropertyChangeListener(listener); 2707 } 2708 } 2709 2710 /** 2711 * Removes the given <code>listener</code> from <code>piece</code> and its children. 2712 */ removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)2713 private void removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) { 2714 if (piece instanceof HomeFurnitureGroup) { 2715 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 2716 removePropertyChangeListener(child, listener); 2717 } 2718 } else { 2719 piece.removePropertyChangeListener(listener); 2720 } 2721 } 2722 2723 /** 2724 * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window. 2725 */ containsDoorsAndWindows(HomePieceOfFurniture piece)2726 private boolean containsDoorsAndWindows(HomePieceOfFurniture piece) { 2727 if (piece instanceof HomeFurnitureGroup) { 2728 for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) { 2729 if (containsDoorsAndWindows(groupPiece)) { 2730 return true; 2731 } 2732 } 2733 return false; 2734 } else { 2735 return piece.isDoorOrWindow(); 2736 } 2737 } 2738 2739 /** 2740 * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase 2741 * with a top cut out shape. 2742 */ containsStaircases(HomePieceOfFurniture piece)2743 private boolean containsStaircases(HomePieceOfFurniture piece) { 2744 if (piece instanceof HomeFurnitureGroup) { 2745 for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) { 2746 if (containsStaircases(groupPiece)) { 2747 return true; 2748 } 2749 } 2750 return false; 2751 } else { 2752 return piece.getStaircaseCutOutShape() != null; 2753 } 2754 } 2755 2756 /** 2757 * Adds a room listener to home rooms that updates the children of the given 2758 * <code>group</code>, each time a room is added, updated or deleted. 2759 */ addRoomListener(final Group group)2760 private void addRoomListener(final Group group) { 2761 this.roomChangeListener = new PropertyChangeListener() { 2762 public void propertyChange(PropertyChangeEvent ev) { 2763 Room updatedRoom = (Room)ev.getSource(); 2764 String propertyName = ev.getPropertyName(); 2765 if (Room.Property.FLOOR_COLOR.name().equals(propertyName) 2766 || Room.Property.FLOOR_TEXTURE.name().equals(propertyName) 2767 || Room.Property.FLOOR_SHININESS.name().equals(propertyName) 2768 || Room.Property.CEILING_COLOR.name().equals(propertyName) 2769 || Room.Property.CEILING_TEXTURE.name().equals(propertyName) 2770 || Room.Property.CEILING_SHININESS.name().equals(propertyName)) { 2771 updateObjects(Arrays.asList(new Room [] {updatedRoom})); 2772 } else if (Room.Property.FLOOR_VISIBLE.name().equals(propertyName) 2773 || Room.Property.CEILING_VISIBLE.name().equals(propertyName) 2774 || Room.Property.LEVEL.name().equals(propertyName)) { 2775 updateObjects(home.getRooms()); 2776 groundChangeListener.propertyChange(null); 2777 } else if (Room.Property.POINTS.name().equals(propertyName)) { 2778 if (homeObjectsToUpdate != null) { 2779 // Don't try to optimize if more than one room to update 2780 updateObjects(home.getRooms()); 2781 } else { 2782 updateObjects(Arrays.asList(new Room [] {updatedRoom})); 2783 updateObjects(getHomeObjects(HomeLight.class)); 2784 // Search the rooms that overlap the updated one 2785 Area oldArea = new Area(getShape((float [][])ev.getOldValue())); 2786 Area newArea = new Area(getShape((float [][])ev.getNewValue())); 2787 Level updatedRoomLevel = updatedRoom.getLevel(); 2788 for (Room room : home.getRooms()) { 2789 Level roomLevel = room.getLevel(); 2790 if (room != updatedRoom 2791 && (roomLevel == null 2792 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5 2793 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) { 2794 Area roomAreaIntersectionWithOldArea = new Area(getShape(room.getPoints())); 2795 Area roomAreaIntersectionWithNewArea = new Area(roomAreaIntersectionWithOldArea); 2796 roomAreaIntersectionWithNewArea.intersect(newArea); 2797 if (!roomAreaIntersectionWithNewArea.isEmpty()) { 2798 updateObjects(Arrays.asList(new Room [] {room})); 2799 } else { 2800 roomAreaIntersectionWithOldArea.intersect(oldArea); 2801 if (!roomAreaIntersectionWithOldArea.isEmpty()) { 2802 updateObjects(Arrays.asList(new Room [] {room})); 2803 } 2804 } 2805 } 2806 } 2807 } 2808 groundChangeListener.propertyChange(null); 2809 updateObjectsLightScope(Arrays.asList(new Room [] {updatedRoom})); 2810 updateObjectsLightScope(getHomeObjects(HomeLight.class)); 2811 } 2812 } 2813 }; 2814 for (Room room : this.home.getRooms()) { 2815 room.addPropertyChangeListener(this.roomChangeListener); 2816 } 2817 this.roomListener = new CollectionListener<Room>() { 2818 public void collectionChanged(CollectionEvent<Room> ev) { 2819 Room room = ev.getItem(); 2820 switch (ev.getType()) { 2821 case ADD : 2822 // Add room to its group at the index indicated by the event 2823 // to ensure the 3D rooms are drawn in the same order as in the plan 2824 addObject(group, room, ev.getIndex(), true, false); 2825 room.addPropertyChangeListener(roomChangeListener); 2826 break; 2827 case DELETE : 2828 deleteObject(room); 2829 room.removePropertyChangeListener(roomChangeListener); 2830 break; 2831 } 2832 updateObjects(home.getRooms()); 2833 groundChangeListener.propertyChange(null); 2834 updateObjectsLightScope(Arrays.asList(new Room [] {room})); 2835 updateObjectsLightScope(getHomeObjects(HomeLight.class)); 2836 } 2837 }; 2838 this.home.addRoomsListener(this.roomListener); 2839 } 2840 2841 /** 2842 * Returns the path matching points. 2843 */ getShape(float [][] points)2844 private GeneralPath getShape(float [][] points) { 2845 GeneralPath path = new GeneralPath(); 2846 path.moveTo(points [0][0], points [0][1]); 2847 for (int i = 1; i < points.length; i++) { 2848 path.lineTo(points [i][0], points [i][1]); 2849 } 2850 path.closePath(); 2851 return path; 2852 } 2853 2854 /** 2855 * Adds a polyline listener to home polylines that updates the children of the given 2856 * <code>group</code>, each time a polyline is added, updated or deleted. 2857 */ addPolylineListener(final Group group)2858 private void addPolylineListener(final Group group) { 2859 this.polylineChangeListener = new PropertyChangeListener() { 2860 public void propertyChange(PropertyChangeEvent ev) { 2861 Polyline polyline = (Polyline)ev.getSource(); 2862 updateObjects(Arrays.asList(new Polyline [] {polyline})); 2863 } 2864 }; 2865 for (Polyline polyline : this.home.getPolylines()) { 2866 polyline.addPropertyChangeListener(this.polylineChangeListener); 2867 } 2868 this.polylineListener = new CollectionListener<Polyline>() { 2869 public void collectionChanged(CollectionEvent<Polyline> ev) { 2870 Polyline polyline = ev.getItem(); 2871 switch (ev.getType()) { 2872 case ADD : 2873 addObject(group, polyline, true, false); 2874 polyline.addPropertyChangeListener(polylineChangeListener); 2875 break; 2876 case DELETE : 2877 deleteObject(polyline); 2878 polyline.removePropertyChangeListener(polylineChangeListener); 2879 break; 2880 } 2881 } 2882 }; 2883 this.home.addPolylinesListener(this.polylineListener); 2884 } 2885 2886 /** 2887 * Adds a label listener to home labels that updates the children of the given 2888 * <code>group</code>, each time a label is added, updated or deleted. 2889 */ addLabelListener(final Group group)2890 private void addLabelListener(final Group group) { 2891 this.labelChangeListener = new PropertyChangeListener() { 2892 public void propertyChange(PropertyChangeEvent ev) { 2893 Label label = (Label)ev.getSource(); 2894 updateObjects(Arrays.asList(new Label [] {label})); 2895 } 2896 }; 2897 for (Label label : this.home.getLabels()) { 2898 label.addPropertyChangeListener(this.labelChangeListener); 2899 } 2900 this.labelListener = new CollectionListener<Label>() { 2901 public void collectionChanged(CollectionEvent<Label> ev) { 2902 Label label = ev.getItem(); 2903 switch (ev.getType()) { 2904 case ADD : 2905 addObject(group, label, true, false); 2906 label.addPropertyChangeListener(labelChangeListener); 2907 break; 2908 case DELETE : 2909 deleteObject(label); 2910 label.removePropertyChangeListener(labelChangeListener); 2911 break; 2912 } 2913 } 2914 }; 2915 this.home.addLabelsListener(this.labelListener); 2916 } 2917 2918 /** 2919 * Adds a walls alpha change listener and drawing mode change listener to home 2920 * environment that updates the home scene objects appearance. 2921 */ addEnvironmentListeners()2922 private void addEnvironmentListeners() { 2923 this.wallsAlphaListener = new PropertyChangeListener() { 2924 public void propertyChange(PropertyChangeEvent ev) { 2925 updateObjects(home.getWalls()); 2926 updateObjects(home.getRooms()); 2927 } 2928 }; 2929 this.home.getEnvironment().addPropertyChangeListener( 2930 HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener); 2931 this.drawingModeListener = new PropertyChangeListener() { 2932 public void propertyChange(PropertyChangeEvent ev) { 2933 updateObjects(home.getWalls()); 2934 updateObjects(home.getRooms()); 2935 updateObjects(getHomeObjects(HomePieceOfFurniture.class)); 2936 } 2937 }; 2938 this.home.getEnvironment().addPropertyChangeListener( 2939 HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener); 2940 } 2941 2942 /** 2943 * Adds to <code>group</code> a branch matching <code>homeObject</code>. 2944 */ addObject(Group group, Selectable homeObject, boolean listenToHomeUpdates, boolean waitForLoading)2945 private Node addObject(Group group, Selectable homeObject, boolean listenToHomeUpdates, boolean waitForLoading) { 2946 return addObject(group, homeObject, -1, listenToHomeUpdates, waitForLoading); 2947 } 2948 2949 /** 2950 * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>. 2951 * If <code>index</code> is equal to -1, <code>homeObject</code> will be added at the end of the group. 2952 */ addObject(Group group, Selectable homeObject, int index, boolean listenToHomeUpdates, boolean waitForLoading)2953 private Node addObject(Group group, Selectable homeObject, int index, 2954 boolean listenToHomeUpdates, boolean waitForLoading) { 2955 Object3DBranch object3D = createObject3D(homeObject, waitForLoading); 2956 if (listenToHomeUpdates) { 2957 this.homeObjects.put(homeObject, object3D); 2958 } 2959 if (index == -1) { 2960 group.addChild(object3D); 2961 } else { 2962 group.insertChild(object3D, index); 2963 } 2964 clearPrintedImageCache(); 2965 return object3D; 2966 } 2967 2968 /** 2969 * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture. 2970 */ addPieceOfFurniture(Group group, HomePieceOfFurniture piece, boolean listenToHomeUpdates, boolean waitForLoading)2971 private void addPieceOfFurniture(Group group, HomePieceOfFurniture piece, boolean listenToHomeUpdates, boolean waitForLoading) { 2972 if (piece instanceof HomeFurnitureGroup) { 2973 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 2974 addPieceOfFurniture(group, child, listenToHomeUpdates, waitForLoading); 2975 } 2976 } else { 2977 addObject(group, piece, listenToHomeUpdates, waitForLoading); 2978 } 2979 } 2980 2981 /** 2982 * Returns the 3D object matching the given home object. If <code>waitForLoading</code> 2983 * is <code>true</code> the resources used by the returned 3D object should be ready to be displayed. 2984 * @deprecated Subclasses which used to override this method must be updated to create an instance of 2985 * a {@link Object3DFactory factory} and give it as parameter to the constructor of this class. 2986 */ createObject3D(Selectable homeObject, boolean waitForLoading)2987 private Object3DBranch createObject3D(Selectable homeObject, 2988 boolean waitForLoading) { 2989 return (Object3DBranch)this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading); 2990 } 2991 2992 /** 2993 * Detaches from the scene the branch matching <code>homeObject</code>. 2994 */ deleteObject(Selectable homeObject)2995 private void deleteObject(Selectable homeObject) { 2996 this.homeObjects.get(homeObject).detach(); 2997 this.homeObjects.remove(homeObject); 2998 if (this.homeObjectsToUpdate != null 2999 && this.homeObjectsToUpdate.contains(homeObject)) { 3000 this.homeObjectsToUpdate.remove(homeObject); 3001 } 3002 clearPrintedImageCache(); 3003 } 3004 3005 /** 3006 * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group. 3007 */ deletePieceOfFurniture(HomePieceOfFurniture piece)3008 private void deletePieceOfFurniture(HomePieceOfFurniture piece) { 3009 if (piece instanceof HomeFurnitureGroup) { 3010 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 3011 deletePieceOfFurniture(child); 3012 } 3013 } else { 3014 deleteObject(piece); 3015 } 3016 } 3017 3018 /** 3019 * Updates 3D <code>objects</code> later. Should be invoked from Event Dispatch Thread. 3020 */ updateObjects(Collection<? extends Selectable> objects)3021 private void updateObjects(Collection<? extends Selectable> objects) { 3022 if (this.homeObjectsToUpdate != null) { 3023 this.homeObjectsToUpdate.addAll(objects); 3024 } else { 3025 this.homeObjectsToUpdate = new HashSet<Selectable>(objects); 3026 // Invoke later the update of objects of homeObjectsToUpdate 3027 EventQueue.invokeLater(new Runnable () { 3028 public void run() { 3029 for (Selectable object : homeObjectsToUpdate) { 3030 Object3DBranch objectBranch = homeObjects.get(object); 3031 // Check object wasn't deleted since updateObjects call 3032 if (objectBranch != null) { 3033 objectBranch.update(); 3034 } 3035 } 3036 homeObjectsToUpdate = null; 3037 } 3038 }); 3039 } 3040 clearPrintedImageCache(); 3041 this.approximateHomeBoundsCache = null; 3042 } 3043 3044 /** 3045 * Updates walls that may intersect from the given doors or window. 3046 */ updateIntersectingWalls(HomePieceOfFurniture .... doorOrWindows)3047 private void updateIntersectingWalls(HomePieceOfFurniture ... doorOrWindows) { 3048 Collection<Wall> walls = this.home.getWalls(); 3049 int wallCount = 0; 3050 if (this.homeObjectsToUpdate != null) { 3051 for (Selectable object : this.homeObjectsToUpdate) { 3052 if (object instanceof Wall) { 3053 wallCount++; 3054 } 3055 } 3056 } 3057 // Check if some more walls may require an update 3058 if (wallCount != walls.size()) { 3059 List<Wall> updatedWalls = new ArrayList<Wall>(); 3060 Rectangle2D doorOrWindowBounds = null; 3061 // Compute the approximate bounds of the doors and windows 3062 for (HomePieceOfFurniture doorOrWindow : doorOrWindows) { 3063 float [][] points = doorOrWindow.getPoints(); 3064 if (doorOrWindowBounds == null) { 3065 doorOrWindowBounds = new Rectangle2D.Float(points [0][0], points [0][1], 0, 0); 3066 } else { 3067 doorOrWindowBounds.add(points [0][0], points [0][1]); 3068 } 3069 for (int i = 1; i < points.length; i++) { 3070 doorOrWindowBounds.add(points [i][0], points [i][1]); 3071 } 3072 } 3073 // Search walls that intersect the bounds 3074 for (Wall wall : walls) { 3075 if (wall.intersectsRectangle((float)doorOrWindowBounds.getX(), (float)doorOrWindowBounds.getY(), 3076 (float)doorOrWindowBounds.getX() + (float)doorOrWindowBounds.getWidth(), 3077 (float)doorOrWindowBounds.getY() + (float)doorOrWindowBounds.getHeight())) { 3078 updatedWalls.add(wall); 3079 } 3080 } 3081 updateObjects(updatedWalls); 3082 } 3083 } 3084 3085 /** 3086 * Updates <code>wall</code> geometry and the walls at its end or start. 3087 */ updateWall(Wall wall)3088 private void updateWall(Wall wall) { 3089 Collection<Wall> wallsToUpdate = new ArrayList<Wall>(3); 3090 wallsToUpdate.add(wall); 3091 if (wall.getWallAtStart() != null) { 3092 wallsToUpdate.add(wall.getWallAtStart()); 3093 } 3094 if (wall.getWallAtEnd() != null) { 3095 wallsToUpdate.add(wall.getWallAtEnd()); 3096 } 3097 updateObjects(wallsToUpdate); 3098 } 3099 3100 /** 3101 * Updates the <code>object</code> scope under light later. Should be invoked from Event Dispatch Thread. 3102 */ updateObjectsLightScope(Collection<? extends Selectable> objects)3103 private void updateObjectsLightScope(Collection<? extends Selectable> objects) { 3104 if (home.getEnvironment().getSubpartSizeUnderLight() > 0) { 3105 if (this.lightScopeObjectsToUpdate != null) { 3106 if (objects == null) { 3107 this.lightScopeObjectsToUpdate.clear(); 3108 this.lightScopeObjectsToUpdate.add(null); 3109 } else if (!this.lightScopeObjectsToUpdate.contains(null)) { 3110 this.lightScopeObjectsToUpdate.addAll(objects); 3111 } 3112 } else { 3113 this.lightScopeObjectsToUpdate = new HashSet<Selectable>(); 3114 if (objects == null) { 3115 this.lightScopeObjectsToUpdate.add(null); 3116 } else { 3117 this.lightScopeObjectsToUpdate.addAll(objects); 3118 } 3119 // Invoke later the update of objects of lightScopeObjectsToUpdate 3120 EventQueue.invokeLater(new Runnable () { 3121 public void run() { 3122 if (lightScopeObjectsToUpdate.contains(null)) { 3123 subpartSizeListener.propertyChange(null); 3124 } else if (home.getEnvironment().getSubpartSizeUnderLight() > 0) { 3125 Area lightScopeOutsideWallsArea = getLightScopeOutsideWallsArea(); 3126 for (Selectable object : lightScopeObjectsToUpdate) { 3127 Group object3D = homeObjects.get(object); 3128 if (object3D instanceof HomePieceOfFurniture3D) { 3129 // Add the direct parent of the shape that will be added once loaded 3130 // otherwise scope won't be updated automatically 3131 object3D = (Group)object3D.getChild(0); 3132 } 3133 // Check object wasn't deleted since updateObjects call 3134 if (object3D != null) { 3135 // Add item to scope if one of its points don't belong to lightScopeOutsideWallsArea 3136 boolean objectInOutsideLightScope = false; 3137 for (float [] point : object.getPoints()) { 3138 if (!lightScopeOutsideWallsArea.contains(point [0], point [1])) { 3139 objectInOutsideLightScope = true; 3140 break; 3141 } 3142 } 3143 for (Light light : sceneLights) { 3144 if (light instanceof DirectionalLight) { 3145 if (objectInOutsideLightScope && light.indexOfScope(object3D) == -1) { 3146 light.addScope(object3D); 3147 } else if (!objectInOutsideLightScope && light.indexOfScope(object3D) != -1) { 3148 light.removeScope(object3D); 3149 } 3150 } 3151 } 3152 } 3153 } 3154 } 3155 lightScopeObjectsToUpdate = null; 3156 } 3157 }); 3158 } 3159 } 3160 } 3161 3162 /** 3163 * Adds to <code>homeRoot</code> shapes matching the shadow of furniture at their level. 3164 */ addShadowOnFloor(Group homeRoot, Map<HomePieceOfFurniture, Node> pieces3D)3165 private void addShadowOnFloor(Group homeRoot, Map<HomePieceOfFurniture, Node> pieces3D) { 3166 Comparator<Level> levelComparator = new Comparator<Level>() { 3167 public int compare(Level level1, Level level2) { 3168 return Float.compare(level1.getElevation(), level2.getElevation()); 3169 } 3170 }; 3171 Map<Level, Area> areasOnLevel = new TreeMap<Level, Area>(levelComparator); 3172 // Compute union of the areas of pieces at ground level that are not lights, doors or windows 3173 for (Map.Entry<HomePieceOfFurniture, Node> object3DEntry : pieces3D.entrySet()) { 3174 if (object3DEntry.getKey() instanceof HomePieceOfFurniture) { 3175 HomePieceOfFurniture piece = object3DEntry.getKey(); 3176 // This operation can be lengthy, so give up if thread is interrupted 3177 if (Thread.currentThread().isInterrupted()) { 3178 return; 3179 } 3180 if (piece.getElevation() == 0 3181 && !piece.isDoorOrWindow() 3182 && !(piece instanceof com.eteks.sweethome3d.model.Light)) { 3183 Area pieceAreaOnFloor = ModelManager.getInstance().getAreaOnFloor(object3DEntry.getValue()); 3184 Level level = piece.getLevel(); 3185 if (piece.getLevel() == null) { 3186 level = new Level("Dummy", 0, 0, 0); 3187 } 3188 if (level.isViewableAndVisible()) { 3189 Area areaOnLevel = areasOnLevel.get(level); 3190 if (areaOnLevel == null) { 3191 areaOnLevel = new Area(); 3192 areasOnLevel.put(level, areaOnLevel); 3193 } 3194 areaOnLevel.add(pieceAreaOnFloor); 3195 } 3196 } 3197 } 3198 } 3199 3200 // Create the 3D shape matching computed areas 3201 Shape3D shadow = new Shape3D(); 3202 for (Map.Entry<Level, Area> levelArea : areasOnLevel.entrySet()) { 3203 List<Point3f> coords = new ArrayList<Point3f>(); 3204 List<Integer> stripCounts = new ArrayList<Integer>(); 3205 int pointsCount = 0; 3206 float [] modelPoint = new float[2]; 3207 for (PathIterator it = levelArea.getValue().getPathIterator(null); !it.isDone(); ) { 3208 if (it.currentSegment(modelPoint) == PathIterator.SEG_CLOSE) { 3209 stripCounts.add(pointsCount); 3210 pointsCount = 0; 3211 } else { 3212 coords.add(new Point3f(modelPoint [0], levelArea.getKey().getElevation() + 0.49f, modelPoint [1])); 3213 pointsCount++; 3214 } 3215 it.next(); 3216 } 3217 3218 if (coords.size() > 0) { 3219 GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY); 3220 geometryInfo.setCoordinates (coords.toArray(new Point3f [coords.size()])); 3221 int [] stripCountsArray = new int [stripCounts.size()]; 3222 for (int i = 0; i < stripCountsArray.length; i++) { 3223 stripCountsArray [i] = stripCounts.get(i); 3224 } 3225 geometryInfo.setStripCounts(stripCountsArray); 3226 shadow.addGeometry(geometryInfo.getIndexedGeometryArray()); 3227 } 3228 } 3229 3230 Appearance shadowAppearance = new Appearance(); 3231 shadowAppearance.setColoringAttributes(new ColoringAttributes(new Color3f(), ColoringAttributes.SHADE_FLAT)); 3232 shadowAppearance.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.NICEST, 0.7f)); 3233 shadow.setAppearance(shadowAppearance); 3234 homeRoot.addChild(shadow); 3235 } 3236 } 3237