libelec
A general purpose library of utility functions designed to make it easier to develop addons for the X-Plane flight simulator.
libelec_vis.c
1 /*
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5  */
6 /*
7  * Copyright 2023 Saso Kiselkov. All rights reserved.
8  */
9 
10 #include <XPLMDisplay.h>
11 #include <XPLMProcessing.h>
12 
13 #include <acfutils/helpers.h>
14 #include <acfutils/widget.h>
15 
16 #include "libelec_types_impl.h"
17 #include "libelec_drawing.h"
18 #include "libelec_vis.h"
19 
20 #define WIN_WIDTH 900 /* px */
21 #define WIN_HEIGHT 600 /* px */
22 #define WIN_FPS 20
23 #define WIN_FPS_FAST 30
24 
25 struct libelec_vis_s {
26  const elec_sys_t *sys;
27  mt_cairo_render_t *mtcr;
28  XPLMWindowID win;
29  double pos_scale;
30  double font_sz;
31  double zoom;
32  vect2_t offset;
33  vect2_t mouse_down;
34  vect2_t mouse_prev;
35 
36  mutex_t lock;
37  const elec_comp_t *highlight;
38  const elec_comp_t *selected;
39 
40  XPLMFlightLoopID floop;
41 };
42 
43 static vect2_t
44 comp_info2sz(const elec_comp_info_t *info)
45 {
46  ASSERT(info != NULL);
47 
48  switch (info->type) {
49  case ELEC_BATT:
50  case ELEC_GEN:
51  case ELEC_CB:
52  case ELEC_TIE:
53  case ELEC_DIODE:
54  return (VECT2(3, 3));
55  case ELEC_TRU:
56  case ELEC_INV:
57  case ELEC_XFRMR:
58  case ELEC_LOAD:
59  return (VECT2(3.5, 3.5));
60  case ELEC_BUS:
61  if (info->gui.sz == 0)
62  return (NULL_VECT2);
63  return (VECT2(2, 1 + 2 * info->gui.sz));
64  case ELEC_SHUNT:
65  return (VECT2(6, 2));
66  case ELEC_LABEL_BOX:
67  VERIFY_FAIL();
68  }
69  VERIFY_FAIL();
70 }
71 
72 static vect2_t
73 cursor_coords_xlate(libelec_vis_t *vis, int x, int y)
74 {
75  int left, top, right, bottom, w, h;
76  vect2_t mouse;
77 
78  ASSERT(vis != NULL);
79 
80  XPLMGetWindowGeometry(vis->win, &left, &top, &right, &bottom);
81  w = right - left;
82  h = top - bottom;
83  x = x - left - w / 2;
84  y = top - y - h / 2;
85 
86  mouse = vect2_scmul(VECT2(x, y), 1 / vis->zoom);
87  mouse = vect2_sub(mouse, vect2_scmul(vis->offset, 1 / vis->zoom));
88  mouse = vect2_scmul(mouse, 1 / vis->pos_scale);
89 
90  return (mouse);
91 }
92 
93 static const elec_comp_t *
94 hit_test(libelec_vis_t *vis, int x, int y)
95 {
96  vect2_t mouse;
97  ASSERT(vis != NULL);
98 
99  mouse = cursor_coords_xlate(vis, x, y);
100 
101  for (const elec_comp_t *comp = list_head(&vis->sys->comps);
102  comp != NULL; comp = list_next(&vis->sys->comps, comp)) {
103  vect2_t pos, sz;
104 
105  pos = comp->info->gui.pos;
106  if (IS_NULL_VECT(pos) || comp->info->gui.virt ||
107  comp->info->gui.invis) {
108  continue;
109  }
110  sz = comp_info2sz(comp->info);
111  if (mouse.x >= pos.x - sz.x / 2 &&
112  mouse.x < pos.x + sz.x / 2 &&
113  mouse.y >= pos.y - sz.y / 2 &&
114  mouse.y < pos.y + sz.y / 2) {
115  return (comp);
116  }
117  }
118 
119  return (NULL);
120 }
121 
122 static void
123 draw_highlight(cairo_t *cr, libelec_vis_t *vis)
124 {
125  ASSERT(cr != NULL);
126  ASSERT(vis != NULL);
127 
128  mutex_enter(&vis->lock);
129  if (vis->highlight != NULL) {
130  vect2_t pos = vis->highlight->info->gui.pos;
131  vect2_t sz = comp_info2sz(vis->highlight->info);
132 
133  cairo_set_line_width(cr, 3);
134  cairo_set_source_rgb(cr, 0, 0, 0);
135  cairo_rectangle(cr, vis->pos_scale * (pos.x - sz.x / 2),
136  vis->pos_scale * (pos.y - sz.y / 2),
137  vis->pos_scale * sz.x, vis->pos_scale * sz.y);
138  cairo_stroke(cr);
139 
140  cairo_set_line_width(cr, 2);
141  cairo_set_source_rgb(cr, 0, 1, 1);
142  cairo_rectangle(cr, vis->pos_scale * (pos.x - sz.x / 2),
143  vis->pos_scale * (pos.y - sz.y / 2),
144  vis->pos_scale * sz.x, vis->pos_scale * sz.y);
145  cairo_stroke(cr);
146  }
147  mutex_exit(&vis->lock);
148 }
149 
150 static void
151 draw_selected(cairo_t *cr, libelec_vis_t *vis)
152 {
153  ASSERT(cr != NULL);
154  ASSERT(vis != NULL);
155 
156  mutex_enter(&vis->lock);
157  if (vis->selected) {
158  libelec_draw_comp_info(vis->selected, cr, vis->pos_scale,
159  vis->font_sz, vis->selected->info->gui.pos);
160  }
161  mutex_exit(&vis->lock);
162 }
163 
164 static void
165 render_cb(cairo_t *cr, unsigned w, unsigned h, void *userinfo)
166 {
167  libelec_vis_t *vis;
168 
169  ASSERT(cr != NULL);
170  UNUSED(w);
171  UNUSED(h);
172  ASSERT(userinfo != NULL);
173  vis = userinfo;
174 
175  cairo_select_font_face(cr, "monospace", CAIRO_FONT_SLANT_NORMAL,
176  CAIRO_FONT_WEIGHT_NORMAL);
177 
178  cairo_identity_matrix(cr);
179 
180  cairo_translate(cr, w / 2, h / 2);
181  cairo_translate(cr, vis->offset.x, vis->offset.y);
182  cairo_scale(cr, vis->zoom, vis->zoom);
183  cairo_set_source_rgb(cr, 1, 1, 1);
184  cairo_paint(cr);
185  libelec_draw_layout(vis->sys, cr, vis->pos_scale, vis->font_sz);
186 
187  draw_highlight(cr, vis);
188  draw_selected(cr, vis);
189 }
190 
191 static void
192 recreate_mtcr(libelec_vis_t *vis)
193 {
194  int left, top, right, bottom;
195 
196  ASSERT(vis != NULL);
197 
198  if (vis->mtcr != NULL)
199  mt_cairo_render_fini(vis->mtcr);
200  ASSERT(vis->win != NULL);
201  XPLMGetWindowGeometry(vis->win, &left, &top, &right, &bottom);
202  vis->mtcr = mt_cairo_render_init(right - left, top - bottom, WIN_FPS,
203  NULL, render_cb, NULL, vis);
204 }
205 
206 static int
207 win_click(XPLMWindowID win, int x, int y, XPLMMouseStatus mouse, void *refcon)
208 {
209  int left, top, x_rel, y_rel;
210 
211  libelec_vis_t* vis;
212  vect2_t off;
213 
214  ASSERT(win != NULL);
215  ASSERT(refcon != NULL);
216  vis = refcon;
217 
218  XPLMGetWindowGeometry(vis->win, &left, &top, NULL, NULL);
219  x_rel = x - left;
220  y_rel = top - y;
221 
222  if (mouse == xplm_MouseDown)
223  vis->mouse_prev = vis->mouse_down = VECT2(x_rel, y_rel);
224  off = vect2_sub(VECT2(x_rel, y_rel), vis->mouse_prev);
225  vis->offset = vect2_add(vis->offset, off);
226  vis->mouse_prev = VECT2(x_rel, y_rel);
227 
228  /* Increase rendering rate while dragging */
229  if (mouse == xplm_MouseDown || mouse == xplm_MouseDrag) {
230  if (mt_cairo_render_get_fps(vis->mtcr) != WIN_FPS_FAST)
231  mt_cairo_render_set_fps(vis->mtcr, WIN_FPS_FAST);
232  } else {
233  mt_cairo_render_set_fps(vis->mtcr, WIN_FPS);
234  }
235  if (mouse == xplm_MouseUp &&
236  vect2_abs(vect2_sub(vis->mouse_down, vis->mouse_prev)) < 4) {
237  mutex_enter(&vis->lock);
238  vis->selected = hit_test(vis, x, y);
239  mutex_exit(&vis->lock);
240  }
241 
242  return (1);
243 }
244 
245 static int
246 win_wheel(XPLMWindowID win, int x, int y, int wheel, int clicks, void *refcon)
247 {
248  int left, top, right, bottom, w, h;
249  libelec_vis_t *vis;
250 
251  ASSERT(win != NULL);
252  UNUSED(x);
253  UNUSED(y);
254  ASSERT(refcon != NULL);
255  vis = refcon;
256 
257  if (wheel != 0)
258  return (1);
259 
260 
261  XPLMGetWindowGeometry(vis->win, &left, &top, &right, &bottom);
262  w = right - left;
263  h = top - bottom;
264  UNUSED(w);
265  UNUSED(h);
266 
267  /*
268  * Limit the zoom range
269  */
270  if (vis->zoom > 10)
271  clicks = MIN(clicks, 0);
272  if (vis->zoom < 0.1)
273  clicks = MAX(clicks, 0);
274  for (; clicks > 0; clicks--) {
275  vis->zoom *= 1.25;
276  vis->offset = vect2_scmul(vis->offset, 1.25);
277  }
278  for (; clicks < 0; clicks++) {
279  vis->zoom /= 1.25;
280  vis->offset = vect2_scmul(vis->offset, 1.0 / 1.25);
281  }
282 
283  return (1);
284 }
285 
286 static void
287 win_draw(XPLMWindowID win, void *refcon)
288 {
289  int left, top, right, bottom, w, h;
290  libelec_vis_t *vis;
291 
292  ASSERT(win != NULL);
293  ASSERT(refcon != NULL);
294  vis = refcon;
295 
296  XPLMGetWindowGeometry(vis->win, &left, &top, &right, &bottom);
297  w = right - left;
298  h = top - bottom;
299  ASSERT(vis->mtcr != NULL);
300  if (w != (int)mt_cairo_render_get_width(vis->mtcr) ||
301  h != (int)mt_cairo_render_get_height(vis->mtcr)) {
302  recreate_mtcr(vis);
303  }
304  mt_cairo_render_draw(vis->mtcr, VECT2(left, bottom), VECT2(w, h));
305 }
306 
307 static XPLMCursorStatus
308 win_cursor(XPLMWindowID win, int x, int y, void *refcon)
309 {
310  libelec_vis_t *vis;
311 
312  ASSERT(win != NULL);
313  ASSERT(refcon != NULL);
314  vis = refcon;
315 
316  mutex_enter(&vis->lock);
317  vis->highlight = hit_test(vis, x, y);
318  mutex_exit(&vis->lock);
319 
320  return (xplm_CursorArrow);
321 }
322 
323 static float
324 vis_floop_cb(UNUSED_ATTR float unused1, UNUSED_ATTR float unused2,
325  UNUSED_ATTR int unused3, void *refcon)
326 {
327  libelec_vis_t *vis;
328 
329  ASSERT(refcon != NULL);
330  vis = refcon;
331 
332  if (!libelec_vis_is_open(vis) && vis->mtcr != NULL) {
333  mt_cairo_render_fini(vis->mtcr);
334  vis->mtcr = NULL;
335  /*
336  * Stop the flight loop callback, we will reschedule it
337  * again when the window is re-opened.
338  */
339  return (0);
340  }
341  return (-1);
342 }
343 
364 libelec_vis_t *
365 libelec_vis_new(const elec_sys_t *sys, double pos_scale, double font_sz)
366 {
367  libelec_vis_t *vis = safe_calloc(1, sizeof (*vis));
368  XPLMCreateWindow_t cr = {
369  .structSize = sizeof (cr),
370  .left = 100,
371  .top = 100 + WIN_HEIGHT,
372  .right = 100 + WIN_WIDTH,
373  .bottom = 100,
374  .handleMouseClickFunc = win_click,
375  .handleMouseWheelFunc = win_wheel,
376  .handleCursorFunc = win_cursor,
377  .drawWindowFunc = win_draw,
378  .decorateAsFloatingWindow = xplm_WindowDecorationRoundRectangle,
379  .layer = xplm_WindowLayerFloatingWindows,
380  .refcon = vis
381  };
382  XPLMCreateFlightLoop_t floop = {
383  .structSize = sizeof (floop),
384  .phase = xplm_FlightLoop_Phase_BeforeFlightModel,
385  .callbackFunc = vis_floop_cb,
386  .refcon = vis
387  };
388 
389  ASSERT(sys != NULL);
390 
391  vis->sys = sys;
392  vis->win = XPLMCreateWindowEx(&cr);
393  ASSERT(vis->win != NULL);
394  vis->pos_scale = pos_scale;
395  vis->font_sz = font_sz;
396  vis->zoom = 1;
397  mutex_init(&vis->lock);
398  vis->floop = XPLMCreateFlightLoop(&floop);
399 
400  XPLMSetWindowTitle(vis->win, "Electrical Network");
401  XPLMSetWindowResizingLimits(vis->win, 200, 200, 100000, 100000);
402  classic_win_center(vis->win);
403 
404  return (vis);
405 }
406 
411 void
412 libelec_vis_destroy(libelec_vis_t *vis)
413 {
414  if (vis == NULL)
415  return;
416 
417  if (vis->mtcr != NULL)
418  mt_cairo_render_fini(vis->mtcr);
419  mutex_destroy(&vis->lock);
420  XPLMDestroyWindow(vis->win);
421  XPLMDestroyFlightLoop(vis->floop);
422 
423  ZERO_FREE(vis);
424 }
425 
433 void
434 libelec_vis_set_offset(libelec_vis_t *vis, vect2_t offset)
435 {
436  ASSERT(vis != NULL);
437  vis->offset = offset;
438 }
439 
443 vect2_t
444 libelec_vis_get_offset(const libelec_vis_t *vis)
445 {
446  ASSERT(vis != NULL);
447  return (vis->offset);
448 }
449 
453 bool
454 libelec_vis_is_open(libelec_vis_t *vis)
455 {
456  ASSERT(vis != NULL);
457  ASSERT(vis->win != NULL);
458  return (XPLMGetWindowIsVisible(vis->win));
459 }
460 
466 void
467 libelec_vis_open(libelec_vis_t *vis)
468 {
469  ASSERT(vis != NULL);
470  ASSERT(vis->win != NULL);
471 
472  if (!XPLMGetWindowIsVisible(vis->win)) {
473  recreate_mtcr(vis);
474  mt_cairo_render_once_wait(vis->mtcr);
475  XPLMSetWindowIsVisible(vis->win, true);
476  XPLMScheduleFlightLoop(vis->floop, -1, true);
477  }
478  window_follow_VR(vis->win);
479 }
480 
491 void
492 libelec_vis_close(libelec_vis_t *vis)
493 {
494  ASSERT(vis != NULL);
495  ASSERT(vis->win != NULL);
496  XPLMSetWindowIsVisible(vis->win, false);
497 }
@ ELEC_SHUNT
Definition: libelec.h:163
@ ELEC_TIE
Definition: libelec.h:194
@ ELEC_LABEL_BOX
Definition: libelec.h:207
@ ELEC_DIODE
Definition: libelec.h:201
@ ELEC_LOAD
Definition: libelec.h:113
@ ELEC_BUS
Definition: libelec.h:121
@ ELEC_GEN
Definition: libelec.h:73
@ ELEC_TRU
Definition: libelec.h:89
@ ELEC_INV
Definition: libelec.h:96
@ ELEC_CB
Definition: libelec.h:147
@ ELEC_BATT
Definition: libelec.h:64
@ ELEC_XFRMR
Definition: libelec.h:103
void libelec_draw_comp_info(const elec_comp_t *comp, cairo_t *cr, double pos_scale, double font_sz, vect2_t pos)
void libelec_draw_layout(const elec_sys_t *sys, cairo_t *cr, double pos_scale, double font_sz)
void libelec_vis_destroy(libelec_vis_t *vis)
Definition: libelec_vis.c:412
void libelec_vis_set_offset(libelec_vis_t *vis, vect2_t offset)
Definition: libelec_vis.c:434
libelec_vis_t * libelec_vis_new(const elec_sys_t *sys, double pos_scale, double font_sz)
Definition: libelec_vis.c:365
bool libelec_vis_is_open(libelec_vis_t *vis)
Definition: libelec_vis.c:454
void libelec_vis_open(libelec_vis_t *vis)
Definition: libelec_vis.c:467
void libelec_vis_close(libelec_vis_t *vis)
Definition: libelec_vis.c:492
vect2_t libelec_vis_get_offset(const libelec_vis_t *vis)
Definition: libelec_vis.c:444