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