Fixed in room navigation.
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / ViewPanel.java
1 /*
2  * Sixth 3D engine. Copyright ©2012-2018, Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of version 3 of the GNU Lesser General Public License
6  * or later as published by the Free Software Foundation.
7  *
8  */
9
10 package eu.svjatoslav.sixth.e3d.gui;
11
12 import eu.svjatoslav.sixth.e3d.gui.humaninput.HIDInputTracker;
13 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack;
14 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
15 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
16
17 import javax.swing.*;
18 import java.awt.*;
19 import java.awt.event.ComponentEvent;
20 import java.awt.event.ComponentListener;
21 import java.util.Set;
22 import java.util.Timer;
23 import java.util.concurrent.ConcurrentHashMap;
24
25 /**
26  * Java Swing GUI panel that contains canvas for 3D rendering.
27  */
28 public class ViewPanel extends JPanel implements ComponentListener {
29     private static final long serialVersionUID = 1683277888885045387L;
30     public Color backgroundColor = Color.BLACK;
31     private final HIDInputTracker HIDInputTracker = new HIDInputTracker(this);
32     private final KeyboardFocusStack keyboardFocusStack;
33     private final Avatar avatar = new Avatar();
34     private final ShapeCollection rootShapeCollection = new ShapeCollection();
35     private final Set<ViewRenderListener> viewRenderListeners = ConcurrentHashMap.newKeySet();
36     /**
37      * Last time this view was updated.
38      */
39     private long lastUpdateMillis = 0;
40     private Timer canvasUpdateTimer;
41     private ViewUpdateTimerTask canvasUpdateTimerTask;
42     private RenderingContext renderingContext = null;
43     /**
44      * UI component that mouse is currently hovering over.
45      */
46     private MouseInteractionController currentMouseOverComponent;
47     /**
48      * Currently target FPS for this view. It can be changed at runtime. Also when nothing
49      * changes in the view, then frames are not really repainted.
50      */
51     private int targetFPS = 30;
52     /**
53      * Set to true if it is known than next frame reeds to be painted. Flag is cleared
54      * immediately after frame got updated.
55      */
56     private boolean viewRepaintNeeded = true;
57     public ViewPanel() {
58         viewRenderListeners.add(avatar);
59         viewRenderListeners.add(HIDInputTracker);
60
61         keyboardFocusStack = new KeyboardFocusStack(this);
62
63         initializePanelLayout();
64
65         setFrameRate(targetFPS);
66
67         addComponentListener(this);
68     }
69
70     public Avatar getAvatar() {
71         return avatar;
72     }
73
74     public KeyboardFocusStack getKeyboardFocusStack() {
75         return keyboardFocusStack;
76     }
77
78     public ShapeCollection getRootShapeCollection() {
79         return rootShapeCollection;
80     }
81
82     public HIDInputTracker getHIDInputTracker() {
83         return HIDInputTracker;
84     }
85
86     public void addViewUpdateListener(final ViewRenderListener listener) {
87         viewRenderListeners.add(listener);
88     }
89
90     @Override
91     public void componentHidden(final ComponentEvent e) {
92
93     }
94
95     @Override
96     public void componentMoved(final ComponentEvent e) {
97
98     }
99
100     @Override
101     public void componentResized(final ComponentEvent e) {
102         viewRepaintNeeded = true;
103     }
104
105     @Override
106     public void componentShown(final ComponentEvent e) {
107         viewRepaintNeeded = true;
108     }
109
110     @Override
111     public Dimension getMaximumSize() {
112         return getPreferredSize();
113     }
114
115     @Override
116     public Dimension getMinimumSize() {
117         return getPreferredSize();
118     }
119
120     @Override
121     public java.awt.Dimension getPreferredSize() {
122         return new java.awt.Dimension(640, 480);
123     }
124
125     public RenderingContext getRenderingContext() {
126         return renderingContext;
127     }
128
129     private void handleDetectedComponentMouseEvents() {
130         if (renderingContext.clickedItem != null) {
131             if (renderingContext.mouseClick.button == 0) {
132                 // mouse over
133                 if (currentMouseOverComponent == null) {
134                     currentMouseOverComponent = renderingContext.clickedItem;
135                     currentMouseOverComponent.mouseEntered();
136                     viewRepaintNeeded = true;
137                 } else if (currentMouseOverComponent != renderingContext.clickedItem) {
138                     currentMouseOverComponent.mouseExited();
139                     currentMouseOverComponent = renderingContext.clickedItem;
140                     currentMouseOverComponent.mouseEntered();
141                     viewRepaintNeeded = true;
142                 }
143             } else {
144                 // mouse click
145                 renderingContext.clickedItem.mouseClicked();
146                 viewRepaintNeeded = true;
147             }
148         } else if (currentMouseOverComponent != null) {
149             currentMouseOverComponent.mouseExited();
150             viewRepaintNeeded = true;
151             currentMouseOverComponent = null;
152         }
153     }
154
155     private void initializePanelLayout() {
156         setFocusCycleRoot(true);
157         setOpaque(true);
158         setFocusable(true);
159         setDoubleBuffered(false);
160         setVisible(true);
161         requestFocusInWindow();
162     }
163
164     private void renderFrame() {
165         if (isNewRenderingContextNeeded())
166             renderingContext = new RenderingContext(getWidth(), getHeight());
167
168         // paint root geometry collection to the offscreen render buffer
169         clearCanvas();
170         rootShapeCollection.paint(this, renderingContext);
171
172         // draw rendered offscreen buffer to visible screen
173         final Graphics graphics = getGraphics();
174         if (graphics != null)
175             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
176     }
177
178     private void clearCanvas() {
179         renderingContext.graphics.setColor(backgroundColor);
180         renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
181     }
182
183     private boolean isNewRenderingContextNeeded() {
184         return (renderingContext == null)
185                 || (renderingContext.width != getWidth())
186                 || (renderingContext.height != getHeight());
187     }
188
189     /**
190      * Calling this methods tells 3D engine that current 3D view needs to be
191      * repainted on first opportunity.
192      */
193     public void repaintDuringNextViewUpdate() {
194         viewRepaintNeeded = true;
195     }
196
197     public void setFrameRate(final int frameRate) {
198         if (canvasUpdateTimerTask != null) {
199             canvasUpdateTimerTask.cancel();
200             canvasUpdateTimerTask = null;
201
202             canvasUpdateTimer.cancel();
203             canvasUpdateTimer = null;
204         }
205
206         targetFPS = frameRate;
207
208         if (frameRate > 0) {
209             canvasUpdateTimer = new Timer();
210             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
211
212             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
213                     1000 / frameRate);
214         }
215     }
216
217     public void stop() {
218         if (canvasUpdateTimerTask != null) {
219             canvasUpdateTimerTask.cancel();
220             canvasUpdateTimerTask = null;
221         }
222
223         if (canvasUpdateTimer != null) {
224             canvasUpdateTimer.cancel();
225             canvasUpdateTimer = null;
226         }
227     }
228
229     /**
230      * This method is executed by periodic timer task, in frequency according to
231      * defined frame rate.
232      * <p>
233      * It tells view to update itself. View can decide if actual re-rendering of
234      * graphics is needed.
235      */
236     void updateView() {
237         if (renderingContext != null) {
238             renderingContext.mouseClick = null;
239             renderingContext.clickedItem = null;
240         }
241
242         final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate();
243
244         boolean renderFrame = notifyViewRenderListeners(millisecondsPassedSinceLastUpdate);
245
246         if (viewRepaintNeeded) {
247             viewRepaintNeeded = false;
248             renderFrame = true;
249         }
250
251         // abort rendering if window size is invalid
252         if ((getWidth() <= 0) || (getHeight() <= 0))
253             renderFrame = false;
254
255         if (renderFrame) {
256             renderFrame();
257             handleDetectedComponentMouseEvents();
258         }
259     }
260
261     private boolean notifyViewRenderListeners(int millisecondsPassedSinceLastUpdate) {
262         boolean reRenderFrame = false;
263         for (final ViewRenderListener listener : viewRenderListeners)
264             if (listener.beforeRender(this, millisecondsPassedSinceLastUpdate))
265                 reRenderFrame = true;
266         return reRenderFrame;
267     }
268
269     private int getMillisecondsPassedSinceLastUpdate() {
270         final long currentTime = System.currentTimeMillis();
271
272         if (lastUpdateMillis == 0)
273             lastUpdateMillis = currentTime;
274
275         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
276         lastUpdateMillis = currentTime;
277         return millisecondsPassedSinceLastUpdate;
278     }
279
280     public void addViewRenderListener(ViewRenderListener viewRenderListener) {
281         viewRenderListeners.add(viewRenderListener);
282     }
283
284     public void removeViewRenderListener(ViewRenderListener viewRenderListener) {
285         viewRenderListeners.remove(viewRenderListener);
286     }
287
288 }