84d45293f38aa19095ee72b5d1ef99c9c18f23d4
[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.objectUnderMouse != null) {
131             if (renderingContext.mouseEvent.button == 0) {
132                 // mouse over
133                 if (currentMouseOverComponent == null) {
134                     currentMouseOverComponent = renderingContext.objectUnderMouse;
135                     viewRepaintNeeded |= currentMouseOverComponent.mouseEntered();
136                 } else if (currentMouseOverComponent != renderingContext.objectUnderMouse) {
137                     viewRepaintNeeded |= currentMouseOverComponent.mouseExited();
138                     currentMouseOverComponent = renderingContext.objectUnderMouse;
139                     viewRepaintNeeded |= currentMouseOverComponent.mouseEntered();
140                 }
141             } else {
142                 // mouse click
143                 viewRepaintNeeded |= renderingContext.objectUnderMouse.mouseClicked();
144             }
145         } else if (currentMouseOverComponent != null) {
146             viewRepaintNeeded |= currentMouseOverComponent.mouseExited();
147             currentMouseOverComponent = null;
148         }
149     }
150
151     private void initializePanelLayout() {
152         setFocusCycleRoot(true);
153         setOpaque(true);
154         setFocusable(true);
155         setDoubleBuffered(false);
156         setVisible(true);
157         requestFocusInWindow();
158     }
159
160     private void renderFrame() {
161
162         // paint root geometry collection to the offscreen render buffer
163         clearCanvas();
164         rootShapeCollection.paint(this, renderingContext);
165
166         // draw rendered offscreen buffer to visible screen
167         final Graphics graphics = getGraphics();
168         if (graphics != null)
169             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
170     }
171
172     private void clearCanvas() {
173         renderingContext.graphics.setColor(backgroundColor);
174         renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
175     }
176
177     /**
178      * Calling this methods tells 3D engine that current 3D view needs to be
179      * repainted on first opportunity.
180      */
181     public void repaintDuringNextViewUpdate() {
182         viewRepaintNeeded = true;
183     }
184
185     public void setFrameRate(final int frameRate) {
186         if (canvasUpdateTimerTask != null) {
187             canvasUpdateTimerTask.cancel();
188             canvasUpdateTimerTask = null;
189
190             canvasUpdateTimer.cancel();
191             canvasUpdateTimer = null;
192         }
193
194         targetFPS = frameRate;
195
196         if (frameRate > 0) {
197             canvasUpdateTimer = new Timer();
198             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
199
200             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
201                     1000 / frameRate);
202         }
203     }
204
205     public void stop() {
206         if (canvasUpdateTimerTask != null) {
207             canvasUpdateTimerTask.cancel();
208             canvasUpdateTimerTask = null;
209         }
210
211         if (canvasUpdateTimer != null) {
212             canvasUpdateTimer.cancel();
213             canvasUpdateTimer = null;
214         }
215     }
216
217     /**
218      * This method is executed by periodic timer task, in frequency according to
219      * defined frame rate.
220      * <p>
221      * It tells view to update itself. View can decide if actual re-rendering of
222      * graphics is needed.
223      */
224     void updateView() {
225         maintainRenderingContext();
226
227         final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate();
228
229         boolean renderFrame = notifyViewRenderListeners(millisecondsPassedSinceLastUpdate);
230
231         if (viewRepaintNeeded) {
232             viewRepaintNeeded = false;
233             renderFrame = true;
234         }
235
236         // abort rendering if window size is invalid
237         if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) {
238             renderFrame();
239             handleDetectedComponentMouseEvents();
240         }
241
242     }
243
244     private void maintainRenderingContext() {
245         int panelWidth = getWidth();
246         int panelHeight = getHeight();
247
248         if (panelWidth <= 0 || panelHeight <=0){
249             renderingContext = null;
250             return;
251         }
252
253         if ((renderingContext == null)
254                 || (renderingContext.width != panelWidth)
255                 || (renderingContext.height != panelHeight)) {
256             renderingContext = new RenderingContext(panelWidth, panelHeight);
257         }
258
259         renderingContext.mouseEvent = null;
260         renderingContext.objectUnderMouse = null;
261     }
262
263     private boolean notifyViewRenderListeners(int millisecondsPassedSinceLastUpdate) {
264         boolean reRenderFrame = false;
265         for (final ViewRenderListener listener : viewRenderListeners)
266             if (listener.beforeRender(this, millisecondsPassedSinceLastUpdate))
267                 reRenderFrame = true;
268         return reRenderFrame;
269     }
270
271     private int getMillisecondsPassedSinceLastUpdate() {
272         final long currentTime = System.currentTimeMillis();
273
274         if (lastUpdateMillis == 0)
275             lastUpdateMillis = currentTime;
276
277         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
278         lastUpdateMillis = currentTime;
279         return millisecondsPassedSinceLastUpdate;
280     }
281
282     public void addViewRenderListener(ViewRenderListener viewRenderListener) {
283         viewRenderListeners.add(viewRenderListener);
284     }
285
286     public void removeViewRenderListener(ViewRenderListener viewRenderListener) {
287         viewRenderListeners.remove(viewRenderListener);
288     }
289
290 }