Skip to content

Commit c616be1

Browse files
committed
feat(FPSMonitor): Add FPS monitor in UI elements
1 parent c6f61c4 commit c616be1

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.container {
2+
display: flex;
3+
flex-direction: row;
4+
justify-content: flex-start;
5+
}
6+
7+
.leftPane {
8+
flex: none;
9+
display: flex;
10+
flex-direction: column;
11+
justify-content: flex-start;
12+
align-items: stretch;
13+
}
14+
15+
.rightPane {
16+
flex: 1;
17+
display: grid;
18+
grid-template-columns: auto auto;
19+
grid-auto-rows: 1.5em;
20+
grid-column-gap: 5px;
21+
grid-row-gap: 2px;
22+
padding: 10px;
23+
}
24+
25+
.title {
26+
flex: 1;
27+
font-weight: bold;
28+
padding: 5px 0 0 10px;
29+
}
30+
31+
.graph {
32+
flex: none;
33+
border: solid 1px black;
34+
margin: 10px ;
35+
border-radius: 2px;
36+
overflow: hidden;
37+
}
38+
39+
.label {
40+
font-weight: bold;
41+
text-transform: capitalize;
42+
text-align: right;
43+
align-self: center;
44+
}
45+
46+
.value {
47+
font-style: italic;
48+
text-align: center;
49+
align-self: center;
50+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import macro from 'vtk.js/Sources/macro';
2+
import style from 'vtk.js/Sources/Interaction/UI/FPSMonitor/FPSMonitor.mcss';
3+
4+
const noOp = Function.prototype;
5+
6+
function formatNumbers(n) {
7+
const sections = [];
8+
let size = n;
9+
while (size > 1000) {
10+
sections.push(`000${size % 1000}`.slice(-3));
11+
size = Math.floor(size / 1000);
12+
}
13+
if (size > 0) {
14+
sections.push(size);
15+
}
16+
sections.reverse();
17+
return sections.join("'");
18+
}
19+
20+
// ----------------------------------------------------------------------------
21+
// vtkFPSMonitor methods
22+
// ----------------------------------------------------------------------------
23+
24+
function vtkFPSMonitor(publicAPI, model) {
25+
// Set our className
26+
model.classHierarchy.push('vtkFPSMonitor');
27+
28+
model.fpsMonitorContainer = document.createElement('div');
29+
model.fpsMonitorContainer.setAttribute('class', style.container);
30+
model.fpsMonitorContainer.innerHTML = `
31+
<div class="${style.leftPane}">
32+
<div class="js-title ${style.title}">Mean N/A - Current N/A</div>
33+
<canvas class="js-graph ${style.graph}"></canvas>
34+
</div>
35+
<div class="js-info ${style.rightPane}">
36+
</div>`;
37+
38+
// Extract various containers
39+
model.canvas = model.fpsMonitorContainer.querySelector('.js-graph');
40+
model.title = model.fpsMonitorContainer.querySelector('.js-title');
41+
model.info = model.fpsMonitorContainer.querySelector('.js-info');
42+
43+
// --------------------------------------------------------------------------
44+
// Private methods
45+
// --------------------------------------------------------------------------
46+
47+
function addFrame() {
48+
if (model.interactor) {
49+
const nextFPS = 1 / model.interactor.getLastFrameTime();
50+
model.buffer.push(nextFPS);
51+
model.fpsSum += nextFPS;
52+
while (model.buffer.length > model.bufferSize) {
53+
model.fpsSum -= model.buffer.shift();
54+
}
55+
model.title.innerHTML = `Mean: ${(
56+
model.fpsSum / model.buffer.length
57+
).toFixed(1)} - Current: ${nextFPS.toFixed(0)}`;
58+
publicAPI.render();
59+
}
60+
}
61+
62+
function updateInformations() {
63+
const infoItems = [];
64+
if (model.renderWindow) {
65+
const stats = model.renderWindow.getStatistics();
66+
const keys = Object.keys(stats);
67+
keys.sort();
68+
for (let i = 0; i < keys.length; i++) {
69+
if (keys[i] === 'str') {
70+
continue; // eslint-disable-line
71+
}
72+
if (stats[keys[i]]) {
73+
infoItems.push(
74+
`<label class="${style.label}">${keys[i]}</label><span class="${
75+
style.value
76+
}">${formatNumbers(stats[keys[i]])}</span>`
77+
);
78+
}
79+
}
80+
}
81+
model.info.innerHTML = infoItems.join('');
82+
}
83+
84+
// --------------------------------------------------------------------------
85+
// --------------------------------------------------------------------------
86+
87+
publicAPI.update = () => {
88+
model.canvas.setAttribute('width', model.bufferSize);
89+
model.canvas.setAttribute('height', model.graphHeight);
90+
updateInformations();
91+
publicAPI.render();
92+
};
93+
94+
// --------------------------------------------------------------------------
95+
96+
publicAPI.setRenderWindow = (rw) => {
97+
while (model.subscriptions.length) {
98+
model.subscriptions.pop().unsubscribe();
99+
}
100+
model.renderWindow = rw;
101+
model.interactor = rw ? rw.getInteractor() : null;
102+
103+
if (model.interactor) {
104+
model.subscriptions.push(model.interactor.onAnimation(addFrame));
105+
}
106+
};
107+
108+
// --------------------------------------------------------------------------
109+
110+
publicAPI.setContainer = (el) => {
111+
if (model.container && model.container !== el) {
112+
model.container.removeChild(model.fpsMonitorContainer);
113+
}
114+
if (model.container !== el) {
115+
model.container = el;
116+
if (model.container) {
117+
model.container.appendChild(model.fpsMonitorContainer);
118+
publicAPI.resize();
119+
}
120+
publicAPI.modified();
121+
}
122+
};
123+
124+
// --------------------------------------------------------------------------
125+
126+
publicAPI.render = () => {
127+
if (model.canvas) {
128+
const ctx = model.canvas.getContext('2d');
129+
ctx.clearRect(0, 0, model.canvas.width, model.canvas.height);
130+
// Current fps
131+
ctx.strokeStyle = 'green';
132+
ctx.beginPath();
133+
ctx.moveTo(0, model.canvas.height - model.buffer[0]);
134+
for (let i = 1; i < model.buffer.length; i++) {
135+
ctx.lineTo(i, model.canvas.height - model.buffer[i]);
136+
}
137+
ctx.stroke();
138+
// 60 fps ref
139+
ctx.strokeStyle = 'black';
140+
ctx.beginPath();
141+
ctx.moveTo(0, model.canvas.height - 60);
142+
ctx.lineTo(model.canvas.width, model.canvas.height - 60);
143+
ctx.stroke();
144+
}
145+
};
146+
147+
// --------------------------------------------------------------------------
148+
149+
publicAPI.resize = noOp;
150+
151+
// --------------------------------------------------------------------------
152+
const superDelete = publicAPI.delete;
153+
publicAPI.delete = () => {
154+
publicAPI.setRenderWindow(null);
155+
publicAPI.setContainer(null);
156+
superDelete();
157+
};
158+
159+
// --------------------------------------------------------------------------
160+
model.subscriptions.push(publicAPI.onModified(publicAPI.update));
161+
}
162+
163+
// ----------------------------------------------------------------------------
164+
// Object factory
165+
// ----------------------------------------------------------------------------
166+
167+
const DEFAULT_VALUES = {
168+
bufferSize: 200,
169+
graphHeight: 120,
170+
buffer: [60],
171+
subscriptions: [],
172+
fpsSum: 0,
173+
};
174+
175+
// ----------------------------------------------------------------------------
176+
177+
export function extend(publicAPI, model, initialValues = {}) {
178+
Object.assign(model, DEFAULT_VALUES, initialValues);
179+
180+
// Object methods
181+
macro.obj(publicAPI, model);
182+
macro.get(publicAPI, model, ['fpsMonitorContainer', 'renderWindow']);
183+
macro.setGet(publicAPI, model, ['bufferSize']);
184+
185+
// Object specific methods
186+
vtkFPSMonitor(publicAPI, model);
187+
}
188+
189+
// ----------------------------------------------------------------------------
190+
191+
export const newInstance = macro.newInstance(extend, 'vtkFPSMonitor');
192+
193+
// ----------------------------------------------------------------------------
194+
195+
export default { newInstance, extend };

0 commit comments

Comments
 (0)