/** * Copyright (c) 2003-2009, Xith3D Project Group all rights reserved. * * Portions based on the Java3D interface, Copyright by Sun Microsystems. * Many thanks to the developers of Java3D and Sun Microsystems for their * innovation and design. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the 'Xith3D Project Group' nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A * RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE */ package org.xith3d.selection; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import org.jagatoo.input.devices.components.MouseButton; import org.jagatoo.input.devices.components.MouseButtons; import org.jagatoo.input.events.MouseButtonClickedEvent; import org.jagatoo.input.events.MouseButtonEvent; import org.jagatoo.input.events.MouseButtonPressedEvent; import org.jagatoo.input.events.MouseButtonReleasedEvent; import org.jagatoo.input.events.MouseMovedEvent; import org.jagatoo.input.events.MouseWheelEvent; import org.jagatoo.input.listeners.MouseListener; import org.openmali.vecmath2.Point2i; import org.openmali.vecmath2.Point3f; import org.openmali.vecmath2.Vector3f; import org.xith3d.picking.AllPickListener; import org.xith3d.picking.PickResult; import org.xith3d.picking.PickingLibrary; import org.xith3d.render.Canvas3D; import org.xith3d.render.RenderPass; import org.xith3d.render.RenderPassConfig; import org.xith3d.scenegraph.BranchGroup; import org.xith3d.scenegraph.Group; import org.xith3d.scenegraph.GroupNode; import org.xith3d.scenegraph.Node; import org.xith3d.scenegraph.SceneGraph; import org.xith3d.ui.hud.listeners.HUDPickMissedListener; import org.xith3d.ui.hud.utils.HUDPickResult.HUDPickReason; /** * @author Mathias Henze (aka cylab) */ public class SelectionManager implements MouseListener, AllPickListener, HUDPickMissedListener { public static final int HUD_PICK_MISSED_MASK = HUDPickReason.BUTTON_PRESSED_MASK | HUDPickReason.BUTTON_RELEASED_MASK; private BranchGroup selectionLayer = new BranchGroup(); private MovementConstraints movementContraints = new ViewConstraints(); private HashSet<Selectable> selection = new HashSet<Selectable>(); private HashSet<Selectable> selectedContext = new HashSet<Selectable>(); private BranchGroup pickableBranch; private RenderPassConfig pickablePassConfig; private ArrayList<GroupNode> pickableGroups= new ArrayList<GroupNode>(); private Canvas3D canvas; private ArrayList<SelectionListener> selectionListeners = new ArrayList<SelectionListener>(); private ContextMenuProvider contextMenuProvider; private Point2i pickLocation = new Point2i(); private Point2i lastMouseCoords = new Point2i(); private Point3f lastPosition = new Point3f(); private MouseButton contextMenuTrigger = MouseButtons.RIGHT_BUTTON; private MouseButton movementTrigger = MouseButtons.LEFT_BUTTON; private MouseButton selectionTrigger = MouseButtons.LEFT_BUTTON; private boolean movementTriggerDown; public Point2i getLastMouseCoords() { return lastMouseCoords; } public Point3f getLastPosition() { return lastPosition; } public MouseButton getContextMenuTrigger() { return contextMenuTrigger; } public void setContextMenuTrigger( MouseButton contextMenuTrigger ) { this.contextMenuTrigger = contextMenuTrigger; } public MouseButton getSelectionTrigger() { return selectionTrigger; } public void setSelectionTrigger( MouseButton selectionTrigger ) { this.selectionTrigger = selectionTrigger; } public BranchGroup getSelectionLayer() { return ( selectionLayer ); } public MouseButton getMovementTrigger() { return movementTrigger; } public void setMovementTrigger( MouseButton movementTrigger ) { this.movementTrigger = movementTrigger; } public void addSelectionListener( SelectionListener listener ) { selectionListeners.add( listener ); } public boolean removeSelectionListener( SelectionListener listener ) { return ( selectionListeners.remove( listener ) ); } public void setContextMenuProvider( ContextMenuProvider provider ) { contextMenuProvider = provider; } public ContextMenuProvider getContextMenuProvider() { return contextMenuProvider; } public MovementConstraints getMovementConstraints() { return ( movementContraints ); } public void setMovementConstraints( MovementConstraints movementConstraints ) { this.movementContraints = movementConstraints; } public void bind( BranchGroup pickableBranch, RenderPassConfig rpc, Canvas3D canvas ) { SceneGraph sceneGraph = pickableBranch.getSceneGraph(); if ( sceneGraph == null ) { throw new IllegalStateException( "You can only bind a SelectionManager to a branch contained in a SceneGraph!" ); } this.pickableBranch = pickableBranch; this.pickablePassConfig = rpc; this.pickableGroups.clear(); this.pickableGroups.add( pickableBranch ); this.canvas = canvas; // just to be sure... sceneGraph.removeBranchGraph( selectionLayer ); selectionLayer.removeAllChildren(); // TODO: do I have to remove the selection pass before? RenderPass selectionPass = sceneGraph.addPerspectiveBranch( selectionLayer ); selectionPass.setLayeredModeForced( true ); } public void bind( BranchGroup pickableBranch, Canvas3D canvas ) { bind( pickableBranch, null, canvas ); } public void unbind() { // TODO: do I have to remove the selection pass too? SceneGraph sceneGraph = pickableBranch.getSceneGraph(); if ( sceneGraph != null ) { sceneGraph.removeBranchGraph( selectionLayer ); selectionLayer.removeAllChildren(); this.pickableBranch = null; this.canvas = null; this.pickablePassConfig = null; } } @SuppressWarnings( "unchecked" ) protected void changeSelection( Selectable selectable, boolean replace ) { if ( selection.contains( selectable ) ) { return; } if ( replace ) { if ( selection.size() > 0 ) { // unselect the current selected nodes // TODO: allow for programmatic selection! for ( Iterator<Selectable> it = selection.iterator(); it.hasNext();) { Selectable sel = it.next(); sel.setSelected( this, false ); } selection.clear(); } if ( selectedContext.size() > 0 ) { for ( Iterator<Selectable> it = selectedContext.iterator(); it.hasNext();) { Selectable sel = it.next(); sel.setSelectedContext( this, false ); } selectedContext.clear(); } if ( ( selection.size() > 0 ) || ( selectedContext.size() > 0 ) ) { // notify the listeners for ( int i = 0; i < selectionListeners.size(); i++ ) { SelectionListener listener = selectionListeners.get( i ); listener.selectionChanged( (List<Selectable>) Collections.EMPTY_LIST, (List<Selectable>) Collections.EMPTY_LIST ); } } } // return with a cleared selection if ( selectable == null ) return; // make new selection selection.add( selectable ); @SuppressWarnings("unused") Selectable selectionBound = null; Node current = selectable.getNode(); while ( ( current = current.getParent() ) != null ) { if ( current instanceof Selectable ) { Selectable currentSelectable = (Selectable) current; if ( !selection.contains( currentSelectable ) ) selectedContext.add( currentSelectable ); if ( currentSelectable.isSelectionBound() ) { selectionBound = currentSelectable; break; } break; } Selectable currentSelectable = (Selectable) current.getUserData( Selectable.class ); if ( ( currentSelectable != null ) ) { if ( !selection.contains( currentSelectable ) ) selectedContext.add( currentSelectable ); if ( currentSelectable.isSelectionBound() ) { selectionBound = currentSelectable; break; } break; } } if ( selectable.getNode() instanceof Group ) { findSelectableChilden( (Group) selectable.getNode() ); } ArrayList<Selectable> tmpSelection = new ArrayList<Selectable>( selection.size() ); ArrayList<Selectable> tmpSelectedContext = new ArrayList<Selectable>( selection.size() ); tmpSelection.addAll( selection ); tmpSelectedContext.addAll( selectedContext ); // TODO: allow for programmatic selection! for ( int i = 0; i < tmpSelection.size(); i++ ) { Selectable sel = tmpSelection.get( i ); sel.setSelected( this, true ); } for ( int i = 0; i < tmpSelectedContext.size(); i++ ) { Selectable sel = tmpSelectedContext.get( i ); sel.setSelectedContext( this, true ); } for ( int i = 0; i < selectionListeners.size(); i++ ) { SelectionListener listener = selectionListeners.get( i ); listener.selectionChanged( tmpSelection, tmpSelectedContext ); } } public boolean isBound() { return ( ( pickableBranch != null ) && ( canvas != null ) ); } private Selectable findSelectable( PickResult pickResult ) { Node current = pickResult.getPickHostOrNode(); do { if ( current instanceof Selectable ) return ( (Selectable) current ); Selectable currentSelectable = (Selectable) current.getUserData( Selectable.class ); if ( currentSelectable != null ) return ( currentSelectable ); } while ( ( current = current.getParent() ) != null ); return ( null ); } private void findSelectableChilden( Group group ) { int l = group.numChildren(); for ( int i = 0; i < l; i++ ) { boolean skipChildren= false; Node current = group.getChild( i ); if ( ( current instanceof Selectable ) ) { skipChildren = true; if ( !selection.contains( current ) ) selectedContext.add( (Selectable) current ); } Selectable currentSelectable = (Selectable) current.getUserData( Selectable.class ); if ( ( currentSelectable != null ) ) { skipChildren = true; if ( !selection.contains( currentSelectable ) ) selectedContext.add( currentSelectable ); } if ( !skipChildren && current instanceof Group ) { findSelectableChilden( (Group) current ); } } } public void onMouseButtonPressed( MouseButtonPressedEvent e, MouseButton button ) { lastMouseCoords.set( e.getX(), e.getY() ); } public void onHUDPickMissed( MouseButton button, int x, int y, HUDPickReason pickReason, long when, long meta ) { if ( isBound() ) { if ( ( button == selectionTrigger ) || ( button == contextMenuTrigger ) ) { final int mouseX = lastMouseCoords.getX(); final int mouseY = lastMouseCoords.getY(); pickLocation.set( mouseX, mouseY ); if ( pickablePassConfig != null ) PickingLibrary.pickAll( pickablePassConfig, pickableGroups, canvas, button, mouseX, mouseY, this, null ); else PickingLibrary.pickAll( pickableBranch, canvas, button, mouseX, mouseY, this ); } } } public void onMouseButtonReleased( MouseButtonReleasedEvent e, MouseButton button ) { } public void onMouseButtonClicked( MouseButtonClickedEvent e, MouseButton button, int clickCount ) { } public void onMouseButtonStateChanged( MouseButtonEvent e, MouseButton button, boolean state ) { if ( button == movementTrigger ) movementTriggerDown = state; } public void onMouseMoved( MouseMovedEvent e, int x, int y, int dx, int dy ) { if ( movementTriggerDown && ( selection.size() > 0 ) ) { ArrayList<Selectable> tmpSelection = new ArrayList<Selectable>( selection.size() ); ArrayList<Selectable> tmpSelectedContext = new ArrayList<Selectable>( selection.size() ); tmpSelection.addAll( selection ); tmpSelectedContext.addAll( selectedContext ); Point3f newPosition = Point3f.fromPool(); Vector3f delta = Vector3f.fromPool(); if ( movementContraints != null ) { movementContraints.computeNewPosition( pickablePassConfig, canvas, x, y, lastPosition, newPosition ); delta.set( newPosition ); delta.sub( lastPosition ); lastPosition.set( newPosition ); } else { delta.set( x - lastMouseCoords.getX(), y - lastMouseCoords.getY(), 0f ); } for ( Iterator<Selectable> it = selection.iterator(); it.hasNext();) { Selectable sel = it.next(); sel.onMoved( this, delta ); } for ( int i = 0; i < selectionListeners.size(); i++ ) { SelectionListener listener = selectionListeners.get( i ); listener.selectionMoved( tmpSelection, tmpSelectedContext, delta ); } Point3f.toPool( newPosition ); Vector3f.toPool( delta ); } lastMouseCoords.set( x, y ); } public void onMouseWheelMoved( MouseWheelEvent e, int wheelDelta ) { } public void onPickingMissed( Object userObject, long pickTime ) { changeSelection( null, true ); } public void onObjectsPicked( List<PickResult> pickResults, Object userObject, long pickTime ) { PickResult nearest = null; Selectable nearestSelectable = null; for ( int i = 0; i < pickResults.size(); i++ ) { PickResult pickResult = pickResults.get( i ); Selectable pickedSelectable = findSelectable( pickResult ); if ( ( pickedSelectable != null ) && ( ( nearest == null ) || ( pickResult.getMinimumDistance() < nearest.getMinimumDistance() ) ) ) { nearest = pickResult; nearestSelectable = pickedSelectable; } } if ( nearest != null ) { lastPosition.set( nearest.getPos() ); changeSelection( nearestSelectable, true ); if ( nearest.getButton() == contextMenuTrigger && contextMenuProvider != null ) { ArrayList<Selectable> tmpSelection = new ArrayList<Selectable>( selection.size() ); ArrayList<Selectable> tmpSelectedContext = new ArrayList<Selectable>( selection.size() ); tmpSelection.addAll( selection ); tmpSelectedContext.addAll( selectedContext ); contextMenuProvider.showContextMenu( pickLocation, nearest.getPos(), tmpSelection, tmpSelectedContext ); } } else { changeSelection( null, true ); } } }