Fixed git clone URL
[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     /**
33      * Stores milliseconds when last frame was updated. This is needed to calculate time delta between frames.
34      * Time delta is used to calculate smooth animation.
35      */
36     private long lastUpdateMillis = 0;
37
38     /**
39      * Timer that is used to update canvas at target FPS rate.
40      */
41     private Timer canvasUpdateTimer;
42
43     private ViewUpdateTimerTask canvasUpdateTimerTask;
44     private RenderingContext renderingContext = null;
45
46     /**
47      * Currently target frames per second rate for this view. Target FPS can be changed at runtime.
48      * 3D engine tries to be smart and only repaints screen when there are visible changes.
49      */
50     private int targetFPS = 60;
51
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
58     public ViewPanel() {
59         viewRenderListeners.add(avatar);
60         viewRenderListeners.add(HIDEventTracker);
61
62         keyboardFocusStack = new KeyboardFocusStack(this);
63
64         initializePanelLayout();
65
66         setFrameRate(targetFPS);
67
68         addComponentListener(this);
69     }
70
71     public Avatar getAvatar() {
72         return avatar;
73     }
74
75     public KeyboardFocusStack getKeyboardFocusStack() {
76         return keyboardFocusStack;
77     }
78
79     public ShapeCollection getRootShapeCollection() {
80         return rootShapeCollection;
81     }
82
83     public HIDEventTracker getHIDInputTracker() {
84         return HIDEventTracker;
85     }
86
87     public void addViewUpdateListener(final ViewRenderListener listener) {
88         viewRenderListeners.add(listener);
89     }
90
91     @Override
92     public void componentHidden(final ComponentEvent e) {
93
94     }
95
96     @Override
97     public void componentMoved(final ComponentEvent e) {
98
99     }
100
101     @Override
102     public void componentResized(final ComponentEvent e) {
103         viewRepaintNeeded = true;
104     }
105
106     @Override
107     public void componentShown(final ComponentEvent e) {
108         viewRepaintNeeded = true;
109     }
110
111     @Override
112     public Dimension getMaximumSize() {
113         return getPreferredSize();
114     }
115
116     @Override
117     public Dimension getMinimumSize() {
118         return getPreferredSize();
119     }
120
121     @Override
122     public java.awt.Dimension getPreferredSize() {
123         return new java.awt.Dimension(640, 480);
124     }
125
126     public RenderingContext getRenderingContext() {
127         return renderingContext;
128     }
129
130     private void initializePanelLayout() {
131         setFocusCycleRoot(true);
132         setOpaque(true);
133         setFocusable(true);
134         setDoubleBuffered(false);
135         setVisible(true);
136         requestFocusInWindow();
137     }
138
139     private void renderFrame() {
140         // paint root geometry collection to the offscreen render buffer
141         clearCanvas();
142         rootShapeCollection.paint(this, renderingContext);
143
144         // draw rendered offscreen buffer to visible screen
145         final Graphics graphics = getGraphics();
146         if (graphics != null)
147             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
148     }
149
150     private void clearCanvas() {
151         renderingContext.graphics.setColor(backgroundColor);
152         renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
153     }
154
155     /**
156      * Calling these methods tells 3D engine that current 3D view needs to be
157      * repainted on first opportunity.
158      */
159     public void repaintDuringNextViewUpdate() {
160         viewRepaintNeeded = true;
161     }
162
163     /**
164      * Set target frames per second rate for this view. Target FPS can be changed at runtime.
165      * @param frameRate target frames per second rate for this view.
166      */
167     public void setFrameRate(final int frameRate) {
168         if (canvasUpdateTimerTask != null) {
169             canvasUpdateTimerTask.cancel();
170             canvasUpdateTimerTask = null;
171
172             canvasUpdateTimer.cancel();
173             canvasUpdateTimer = null;
174         }
175
176         targetFPS = frameRate;
177
178         if (frameRate <= 0) return;
179
180         canvasUpdateTimer = new Timer();
181         canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
182
183         // schedule timer task to run in frequency according to defined frame rate
184         canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
185                 1000 / frameRate);
186     }
187
188     /**
189      * Stops rendering of this view.
190      */
191     public void stop() {
192         setFrameRate(0);
193     }
194
195     /**
196      * This method is executed by periodic timer task, in frequency according to
197      * defined frame rate.
198      * <p>
199      * It tells view to update itself. View can decide if actual re-rendering of
200      * graphics is needed.
201      */
202     void ensureThatViewIsUpToDate() {
203         maintainRenderingContext();
204
205         final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate();
206
207         boolean renderFrame = notifyViewRenderListeners(millisecondsPassedSinceLastUpdate);
208
209         if (viewRepaintNeeded) {
210             viewRepaintNeeded = false;
211             renderFrame = true;
212         }
213
214         // abort rendering if window size is invalid
215         if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) {
216             renderFrame();
217             viewRepaintNeeded = renderingContext.handlePossibleComponentMouseEvent();
218         }
219
220     }
221
222     private void maintainRenderingContext() {
223         int panelWidth = getWidth();
224         int panelHeight = getHeight();
225
226         if (panelWidth <= 0 || panelHeight <= 0) {
227             renderingContext = null;
228             return;
229         }
230
231         // create new rendering context if window size has changed
232         if ((renderingContext == null)
233                 || (renderingContext.width != panelWidth)
234                 || (renderingContext.height != panelHeight)) {
235             renderingContext = new RenderingContext(panelWidth, panelHeight);
236         }
237
238         renderingContext.prepareForNewFrameRendering();
239     }
240
241     private boolean notifyViewRenderListeners(int millisecondsPassedSinceLastUpdate) {
242         boolean reRenderFrame = false;
243         for (final ViewRenderListener listener : viewRenderListeners)
244             if (listener.beforeRender(this, millisecondsPassedSinceLastUpdate))
245                 reRenderFrame = true;
246         return reRenderFrame;
247     }
248
249     private int getMillisecondsPassedSinceLastUpdate() {
250         final long currentTime = System.currentTimeMillis();
251
252         if (lastUpdateMillis == 0)
253             lastUpdateMillis = currentTime;
254
255         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
256         lastUpdateMillis = currentTime;
257         return millisecondsPassedSinceLastUpdate;
258     }
259
260     public void addViewRenderListener(ViewRenderListener viewRenderListener) {
261         viewRenderListeners.add(viewRenderListener);
262     }
263
264     public void removeViewRenderListener(ViewRenderListener viewRenderListener) {
265         viewRenderListeners.remove(viewRenderListener);
266     }
267
268 }