/*********************************************************************** * mt4j Copyright (c) 2008 - 2009, C.Ruff, Fraunhofer-Gesellschaft All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * ***********************************************************************/ package org.mt4j.components; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.HashMap; import javax.swing.Timer; import org.mt4j.components.clusters.Cluster; import org.mt4j.components.clusters.ClusterManager; import org.mt4j.components.interfaces.IMTComponent3D; import org.mt4j.input.IHitTestInfoProvider; import org.mt4j.input.inputData.MTInputEvent; import org.mt4j.input.inputProcessors.componentProcessors.dragProcessor.DragProcessor; import org.mt4j.input.inputProcessors.componentProcessors.rotateProcessor.RotateProcessor; import org.mt4j.input.inputProcessors.componentProcessors.scaleProcessor.ScaleProcessor; import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor; import org.mt4j.util.camera.Icamera; import org.mt4j.util.math.Matrix; import processing.core.PApplet; import processing.core.PGraphics; import processing.core.PGraphics3D; /** * MTCanvas is the root node of the component hierarchy of a MT4j scene. * To make a mt4j component visible and interactable you have to add it to * the scene's canvas or to a component which is already attached to the canvas. * The canvas then recursivly updates and draws all attached components each frame. * * @author Christopher Ruff */ public class MTCanvas extends MTComponent implements IHitTestInfoProvider{ /** The cluster manager. */ private ClusterManager clusterManager; /** The last time hit test. */ private long lastTimeHitTest; /** The cache time delta. */ private long cacheTimeDelta; /** The cache clear time. */ private int cacheClearTime; /** The position to component. */ private HashMap<Position, IMTComponent3D> positionToComponent; /** The timer. */ private Timer timer; /** The use hit test cache. */ private boolean useHitTestCache; /** The frustum culling switch. */ private boolean frustumCulling; private int culledObjects = 0; private long lastUpdateTime; /** * The Constructor. * * @param pApplet the applet * @param attachedCamera the attached camera */ public MTCanvas(PApplet pApplet, Icamera attachedCamera) { this(pApplet, "unnamed MT Canvas", attachedCamera); } /** * The Constructor. * * @param pApplet the applet * @param name the name * @param attachedCamera the attached camera */ public MTCanvas(PApplet pApplet, String name, Icamera attachedCamera) { super(pApplet, name, attachedCamera); //Cache settings lastTimeHitTest = 0; cacheTimeDelta = 100; cacheClearTime = 20000; useHitTestCache = true; lastUpdateTime = 0; clusterManager = new ClusterManager(pApplet, this); positionToComponent = new HashMap<Position, IMTComponent3D>(); //Schedule a Timer task to clear the object cache so it wont //get filled infinitely timer = new Timer(cacheClearTime, new ActionListener(){ public void actionPerformed(ActionEvent arg0) { // System.out.println("Hit test chache entries: " + positionToComponent.size() + " ... cleared!"); positionToComponent.clear(); } }); // timer.start(); //FIXME TEST this.useHitTestCache = false; //FIXME TEST DISABLE HIT CACHE // this.setCollidable(false); this.setGestureAllowance(RotateProcessor.class, false); this.setGestureAllowance(ScaleProcessor.class, false); this.setGestureAllowance(TapProcessor.class, false); this.setGestureAllowance(DragProcessor.class, false); this.setPickable(false); //Frustum culling default frustumCulling = false; } @Override protected void destroyComponent() { super.destroyComponent(); if (this.timer != null && timer.isRunning()){ timer.stop(); } if (positionToComponent != null){ positionToComponent.clear(); } } /** * Method for asking the canvas whether and which object is at the specified * screen position. * <p> * IMPORTANT: this method returns the MTCanvas instance if no other object is hit. * This means that the MTCanvas instance acts like the background => Gestures that * are supposed to be performed on the background have to check if they hit the canvas. * And the gestureevents should then have the canvas as their targetComponent! * Also, you have to be careful in other gestures, as even when you dont hit an object, you will * get the mtcanvas returned as the hit component - not null! * <p>Note: if the hit component is part of a cluster, the cluster is returned! * * @param x the screen x coordinate * @param y the screen y coordinate * * @return the object at that position or this MTCanvas instance if no component was hit */ public IMTComponent3D getComponentAt(float x, float y) { IMTComponent3D closest3DComp = null; try{ long now = System.currentTimeMillis(); if (useHitTestCache){ if (now - lastTimeHitTest > cacheTimeDelta){ //If the time since last check surpassed => do new hit-test! //Benchmark the picking // long a = System.nanoTime(); closest3DComp = this.pick(x, y).getNearestPickResult(); //Benchmark the picking // long b = System.nanoTime(); // System.out.println("Time for picking the scene: " + (b-a)); /* for (MTBaseComponent c : pickResult.getPickList()) System.out.println(c.getName()); if (closest3DComp != null) System.out.println("Using: " + closest3DComp.getName()); */ if (closest3DComp == null){ closest3DComp = this; } positionToComponent.put(new Position(x,y), closest3DComp); }else{ //Check whats in the cache IMTComponent3D cachedComp = positionToComponent.get(new Position(x,y)); if (cachedComp != null){ //Use cached obj closest3DComp = cachedComp; positionToComponent.put(new Position(x,y), closest3DComp); }else{ closest3DComp = this.pick(x, y).getNearestPickResult(); if (closest3DComp == null){ closest3DComp = this; } positionToComponent.put(new Position(x,y), closest3DComp); } } }else{//IF no hittest cache is being used closest3DComp = this.pick(x, y).getNearestPickResult(); if (closest3DComp == null){ closest3DComp = this; } } lastTimeHitTest = now; // /*//TODO anders machen..z.b. geclusterte comps einfach als kinder von //�bergeordnetem clusterpoly machen? aber mit clusterPoly.setComposite(TRUE); //Clusterpoly pickable machen damit das hier nicht gebraucht wird? Cluster sel = this.getClusterManager().getCluster(closest3DComp); if (sel != null){ closest3DComp = sel; } // */ // //FIXME TEST for stencil clipped scene windows -> we have to return the scenes canvas for some gestures! // if (closest3DComp != null && closest3DComp instanceof mtClipSceneWindow) }catch(Exception e){ System.err.println("Error while trying to pick an object: "); e.printStackTrace(); } /* if (closest3DComp != null) System.out.println("Picked: '" + closest3DComp.getName() + "' at pos (" + x + "," + y + ")"); else System.out.println("Picked: '" + closest3DComp + "' at pos (" + x + "," + y + ")"); */ return closest3DComp; } /* (non-Javadoc) * @see com.jMT.input.IHitTestInfoProvider#isBackGroundAt(float, float) */ public boolean isBackGroundAt(float x, float y) { return this.getComponentAt(x, y).equals(this); } //FIXME TEST @Override public void updateComponent(long timeDelta) { super.updateComponent(timeDelta); this.lastUpdateTime = timeDelta; } //FIXME TEST private boolean calledFromDrawComponent = false; //FIXME TEST // /* // * Actually this canvases drawComponent method should be called // * ever because the canvas should not be added as a child to any component. // * Anyway - this code will still make it possible to use it as a child of other components // * (non-Javadoc) // * @see org.mt4j.components.MTComponent#drawComponent(processing.core.PGraphics) // */ // @Override // public void drawComponent(PGraphics g) { //FIXME this would draw the canvas 2 times.. // super.drawComponent(g); // // //Call the canvases scenes draw method to also draw // //stuff defined in an overrriden scenes draw method // if (this.getRenderer() instanceof MTApplication){ // MTApplication app = (MTApplication)this.getRenderer(); // Iscene[] scenes = app.getScenes(); // for (int i = 0; i < scenes.length; i++) { // Iscene iscene = scenes[i]; // if (iscene instanceof AbstractScene){ // AbstractScene as = (AbstractScene)iscene; // if (as.getCanvas().equals(this)){ // this.calledFromDrawComponent = true; //// this.drawAndUpdateCanvas(g, this.lastUpdateTime); // as.drawAndUpdate(g, this.lastUpdateTime); // this.calledFromDrawComponent = false; // } // } // } // } // //// this.calledFromDrawComponent = true; //// this.drawAndUpdateCanvas(g, this.lastUpdateTime); //// this.calledFromDrawComponent = false; // } /** * Updates and then draws every visible object in the canvas. * First calls the <code>updateComponent(long timeDelta)</code> method. Then * the <code>drawComponent()</code> method of each object in the scene graph. * Also handles the setting of cameras attached to the objects. * @param graphics * * @param updateTime the time passed since the last update (in ms) */ public void drawAndUpdateCanvas(PGraphics graphics, long updateTime){ this.culledObjects = 0; //FIXME THIS IS A HACK! WE SHOULD REPLACE CLUSTERS WITH NORMAL COMPONENTS INSTEAD! //Update cluster components Cluster[] clusters = getClusterManager().getClusters(); for (int i = 0; i < clusters.length; i++) { Cluster cluster = clusters[i]; cluster.updateComponent(updateTime); } this.drawUpdateRecursive(this, updateTime, graphics); // System.out.println("Culled objects: " + culledObjects); } /** * Draw the whole canvas update recursive. * * @param currentcomp the currentcomp * @param updateTime the update time * @param graphics the renderer */ private void drawUpdateRecursive(MTComponent currentcomp, long updateTime, PGraphics graphics){ if (currentcomp.isVisible()){ //Update current component currentcomp.updateComponent(updateTime); if (currentcomp.getAttachedCamera() != null){ //Saves transformations up to this object graphics.pushMatrix(); //Resets the modelview completely with a new camera matrix currentcomp.getAttachedCamera().update(); if (currentcomp.getParent() != null){ //Applies all transforms up to this components parent //because the new camera wiped out all previous transforms Matrix m = currentcomp.getParent().getGlobalMatrix(); PGraphics3D pgraphics3D = (PGraphics3D)graphics; pgraphics3D.modelview.apply( m.m00, m.m01, m.m02, m.m03, m.m10, m.m11, m.m12, m.m13, m.m20, m.m21, m.m22, m.m23, m.m30, m.m31, m.m32, m.m33 ); } //Apply local transform etc currentcomp.preDraw(graphics); //Check visibility with camera frustum if (frustumCulling){ if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){ if (!this.calledFromDrawComponent){ //FIXME TEST // DRAW THE COMPONENT \\ currentcomp.drawComponent(graphics); } }else{ culledObjects++; //System.out.println("Not visible: " + currentcomp.getName()); } }else{ if (!this.calledFromDrawComponent){ //FIXME TEST // DRAW THE COMPONENT \\ currentcomp.drawComponent(graphics); } } currentcomp.postDraw(graphics); //Draw Children for (MTComponent child : currentcomp.getChildList()) drawUpdateRecursive(child, updateTime, graphics); currentcomp.postDrawChildren(graphics); //Restores the transforms of the previous camera etc graphics.popMatrix(); }else{//If no custom camera was set //TODO in abstactvisiblecomp wird outine �ber gradients und clips //gezeichnet obwohl hier invisble war! FIXME! //evtl applymatrix unapply in eigene methode? dann nur das ausf�hren, kein pre/post draw! //TODO vater an kinder listener -> resize - new geometry -> resize own currentcomp.preDraw(graphics); if (frustumCulling){ //Check visibility with camera frustum if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){ // DRAW THE COMPONENT \\ currentcomp.drawComponent(graphics); }else{ culledObjects++; //System.out.println("Not visible: " + currentcomp.getName()); } }else{ // DRAW THE COMPONENT \\ currentcomp.drawComponent(graphics); } currentcomp.postDraw(graphics); for (MTComponent child : currentcomp.getChildList()) drawUpdateRecursive(child, updateTime, graphics); currentcomp.postDrawChildren(graphics); } }//if visible end } /* (non-Javadoc) * @see org.mt4j.components.MTComponent#processInputEvent(org.mt4j.input.inputData.MTInputEvent) */ @Override public boolean processInputEvent(MTInputEvent inEvt) { //TODO not very elegant - better approach?? if (inEvt.hasTarget()){ if (!inEvt.getTargetComponent().equals(this)){ //Avoid recursion //Send directed event to the target component return inEvt.getTargetComponent().processInputEvent(inEvt); } } // return true; //this.handleEvent //handle in superclass //The MTCanvas get events targeted at him AND events that have no target! return super.processInputEvent(inEvt); } /** * Gets the cluster manager. * * @return the cluster manager */ public ClusterManager getClusterManager() { return clusterManager; } /** * Sets the cluster manager. * * @param selectionManager the new cluster manager */ public void setClusterManager(ClusterManager selectionManager) { this.clusterManager = selectionManager; } /** * Gets the cache time delta. * * @return the cache time delta */ public long getCacheTimeDelta() { return cacheTimeDelta; } /** * If repeated calls to getObjectAt(float x, float y) in MTCanvas class * are called during the provided cacheTimeDelta, the Canvas looks into his * cache instead of querying all objects again * Default value is: 80. * * @param cacheTimeDelta the cache time delta */ public void setCacheTimeDelta(long cacheTimeDelta) { this.cacheTimeDelta = cacheTimeDelta; } /** * Checks if is use hit test cache. * * @return true, if is use hit test cache */ public boolean isUseHitTestCache() { return useHitTestCache; } /** * The canvas can be set to look into a hit test cache if * repeated calls to getComponentAt() with the same coordinates * during a short period of time are made. * This period of time can be set with * <code>setCacheTimeDelta(long cacheTimeDelta)</code> * <p> * This is useful for example when a click is made many gestureanalyzers * call getObjectAt() almost concurrently. * * @param useHitTestCache the use hit test cache */ public void setUseHitTestCache(boolean useHitTestCache) { if (useHitTestCache && !timer.isRunning()) timer.start(); else if (!useHitTestCache && timer.isRunning()) timer.stop(); this.useHitTestCache = useHitTestCache; } /** * Gets the cache clear time. * * @return the cache clear time */ public int getCacheClearTime() { return cacheClearTime; } /** * Sets the time intervals in ms in which the canvas clears its hit test cache * Default value is: 20000 ms * <p> * This is important to prevent the hit test cache from growing indefinitely. * * @param cacheClearTime the cache clear time */ public void setCacheClearTime(int cacheClearTime) { timer.setDelay(cacheClearTime); this.cacheClearTime = cacheClearTime; } public boolean isFrustumCulling() { return frustumCulling; } public void setFrustumCulling(boolean frustumCulling) { this.frustumCulling = frustumCulling; } /** * Class used for the pickobject cache. */ private class Position{ /** The y. */ float x,y; /** * Instantiates a new position. * * @param x the x * @param y the y */ public Position(float x, float y){ this.x = x; this.y = y; } /** * Gets the x. * * @return the x */ public float getX() {return x;} /** * Sets the x. * * @param x the new x */ public void setX(float x) {this.x = x;} /** * Gets the y. * * @return the y */ public float getY() {return y;} /** * Sets the y. * * @param y the new y */ public void setY(float y) {this.y = y;} /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object arg0) { return (arg0 instanceof Position && ((Position)arg0).getX() == this.getX() && ((Position)arg0).getY() == this.getY()); } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return ((int)x+(int)y); } } }