1 /* ______ ___ ___
2 * /\ _ \ /\_ \ /\_ \
3 * \ \ \L\ \\//\ \ \//\ \ __ __ _ __ ___
4 * \ \ __ \ \ \ \ \ \ \ /'__`\ /'_ `\/\`'__\/ __`\
5 * \ \ \/\ \ \_\ \_ \_\ \_/\ __//\ \L\ \ \ \//\ \L\ \
6 * \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
7 * \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
8 * /\____/
9 * \_/__/
10 *
11 * Transformations.
12 *
13 * By Pavel Sountsov.
14 *
15 * See readme.txt for copyright information.
16 */
17
18 #include "allegro5/allegro.h"
19 #include "allegro5/internal/aintern.h"
20 #include "allegro5/internal/aintern_bitmap.h"
21 #include "allegro5/internal/aintern_display.h"
22 #include "allegro5/internal/aintern_system.h"
23 #include "allegro5/internal/aintern_transform.h"
24 #include <math.h>
25
26 /* ALLEGRO_DEBUG_CHANNEL("transformations") */
27
28 /* Function: al_copy_transform
29 */
al_copy_transform(ALLEGRO_TRANSFORM * dest,const ALLEGRO_TRANSFORM * src)30 void al_copy_transform(ALLEGRO_TRANSFORM *dest, const ALLEGRO_TRANSFORM *src)
31 {
32 ASSERT(src);
33 ASSERT(dest);
34
35 memcpy(dest, src, sizeof(ALLEGRO_TRANSFORM));
36 }
37
38 /* Function: al_use_transform
39 */
al_use_transform(const ALLEGRO_TRANSFORM * trans)40 void al_use_transform(const ALLEGRO_TRANSFORM *trans)
41 {
42 ALLEGRO_BITMAP *target = al_get_target_bitmap();
43 ALLEGRO_DISPLAY *display;
44
45 if (!target)
46 return;
47
48 /* Changes to a back buffer should affect the front buffer, and vice versa.
49 * Currently we rely on the fact that in the OpenGL drivers the back buffer
50 * and front buffer bitmaps are exactly the same, and the DirectX driver
51 * doesn't support front buffer bitmaps.
52 */
53
54 if (trans != &target->transform) {
55 al_copy_transform(&target->transform, trans);
56
57 target->inverse_transform_dirty = true;
58 }
59
60 /*
61 * When the drawing is held, we apply the transformations in software,
62 * so the hardware transformation has to be kept at identity.
63 */
64 if (!al_is_bitmap_drawing_held()) {
65 display = _al_get_bitmap_display(target);
66 if (display) {
67 display->vt->update_transformation(display, target);
68 }
69 }
70 }
71
72 /* Function: al_use_projection_transform
73 */
al_use_projection_transform(const ALLEGRO_TRANSFORM * trans)74 void al_use_projection_transform(const ALLEGRO_TRANSFORM *trans)
75 {
76 ALLEGRO_BITMAP *target = al_get_target_bitmap();
77 ALLEGRO_DISPLAY *display;
78
79 if (!target)
80 return;
81
82 /* Memory bitmaps don't support custom projection transforms */
83 if (al_get_bitmap_flags(target) & ALLEGRO_MEMORY_BITMAP)
84 return;
85
86 /* Changes to a back buffer should affect the front buffer, and vice versa.
87 * Currently we rely on the fact that in the OpenGL drivers the back buffer
88 * and front buffer bitmaps are exactly the same, and the DirectX driver
89 * doesn't support front buffer bitmaps.
90 */
91
92 if (trans != &target->transform) {
93 al_copy_transform(&target->proj_transform, trans);
94 }
95
96 display = _al_get_bitmap_display(target);
97 if (display) {
98 display->vt->update_transformation(display, target);
99 }
100 }
101
102 /* Function: al_get_current_transform
103 */
al_get_current_transform(void)104 const ALLEGRO_TRANSFORM *al_get_current_transform(void)
105 {
106 ALLEGRO_BITMAP *target = al_get_target_bitmap();
107
108 if (!target)
109 return NULL;
110
111 return &target->transform;
112 }
113
114 /* Function: al_get_current_projection_transform
115 */
al_get_current_projection_transform(void)116 const ALLEGRO_TRANSFORM *al_get_current_projection_transform(void)
117 {
118 ALLEGRO_BITMAP *target = al_get_target_bitmap();
119
120 if (!target)
121 return NULL;
122
123 return &target->proj_transform;
124 }
125
126 /* Function: al_get_current_inverse_transform
127 */
al_get_current_inverse_transform(void)128 const ALLEGRO_TRANSFORM *al_get_current_inverse_transform(void)
129 {
130 ALLEGRO_BITMAP *target = al_get_target_bitmap();
131
132 if (!target)
133 return NULL;
134
135 if (target->inverse_transform_dirty) {
136 al_copy_transform(&target->inverse_transform, &target->transform);
137 al_invert_transform(&target->inverse_transform);
138 }
139
140 return &target->inverse_transform;
141 }
142
143 /* Function: al_identity_transform
144 */
al_identity_transform(ALLEGRO_TRANSFORM * trans)145 void al_identity_transform(ALLEGRO_TRANSFORM *trans)
146 {
147 ASSERT(trans);
148
149 trans->m[0][0] = 1;
150 trans->m[0][1] = 0;
151 trans->m[0][2] = 0;
152 trans->m[0][3] = 0;
153
154 trans->m[1][0] = 0;
155 trans->m[1][1] = 1;
156 trans->m[1][2] = 0;
157 trans->m[1][3] = 0;
158
159 trans->m[2][0] = 0;
160 trans->m[2][1] = 0;
161 trans->m[2][2] = 1;
162 trans->m[2][3] = 0;
163
164 trans->m[3][0] = 0;
165 trans->m[3][1] = 0;
166 trans->m[3][2] = 0;
167 trans->m[3][3] = 1;
168 }
169
170 /* Function: al_build_transform
171 */
al_build_transform(ALLEGRO_TRANSFORM * trans,float x,float y,float sx,float sy,float theta)172 void al_build_transform(ALLEGRO_TRANSFORM *trans, float x, float y,
173 float sx, float sy, float theta)
174 {
175 float c, s;
176 ASSERT(trans);
177
178 c = cosf(theta);
179 s = sinf(theta);
180
181 trans->m[0][0] = sx * c;
182 trans->m[0][1] = sy * s;
183 trans->m[0][2] = 0;
184 trans->m[0][3] = 0;
185
186 trans->m[1][0] = -sx * s;
187 trans->m[1][1] = sy * c;
188 trans->m[1][2] = 0;
189 trans->m[1][3] = 0;
190
191 trans->m[2][0] = 0;
192 trans->m[2][1] = 0;
193 trans->m[2][2] = 1;
194 trans->m[2][3] = 0;
195
196 trans->m[3][0] = x;
197 trans->m[3][1] = y;
198 trans->m[3][2] = 0;
199 trans->m[3][3] = 1;
200 }
201
202 /* Function: al_build_camera_transform
203 */
al_build_camera_transform(ALLEGRO_TRANSFORM * trans,float position_x,float position_y,float position_z,float look_x,float look_y,float look_z,float up_x,float up_y,float up_z)204 void al_build_camera_transform(ALLEGRO_TRANSFORM *trans,
205 float position_x, float position_y, float position_z,
206 float look_x, float look_y, float look_z,
207 float up_x, float up_y, float up_z)
208 {
209 float x = position_x;
210 float y = position_y;
211 float z = position_z;
212 float xx, xy, xz, xnorm;
213 float yx, yy, yz;
214 float zx, zy, zz, znorm;
215
216 al_identity_transform(trans);
217
218 /* Get the z-axis (direction towards viewer) and normalize it.
219 */
220 zx = x - look_x;
221 zy = y - look_y;
222 zz = z - look_z;
223 znorm = sqrt(zx * zx + zy * zy + zz * zz);
224 if (znorm == 0)
225 return;
226 zx /= znorm;
227 zy /= znorm;
228 zz /= znorm;
229
230 /* Get the x-axis (direction pointing to the right) as the cross product of
231 * the up-vector times the z-axis. We need to normalize it because we do
232 * neither require the up-vector to be normalized nor perpendicular.
233 */
234 xx = up_y * zz - zy * up_z;
235 xy = up_z * zx - zz * up_x;
236 xz = up_x * zy - zx * up_y;
237 xnorm = sqrt(xx * xx + xy * xy + xz * xz);
238 if (xnorm == 0)
239 return;
240 xx /= xnorm;
241 xy /= xnorm;
242 xz /= xnorm;
243
244 /* Now use the cross product of z-axis and x-axis as our y-axis. This can
245 * have a different direction than the original up-vector but it will
246 * already be normalized.
247 */
248 yx = zy * xz - xy * zz;
249 yy = zz * xx - xz * zx;
250 yz = zx * xy - xx * zy;
251
252 /* This is an inverse translation (subtract the camera position) followed by
253 * an inverse rotation (rotate in the opposite direction of the camera
254 * orientation).
255 */
256 trans->m[0][0] = xx;
257 trans->m[1][0] = xy;
258 trans->m[2][0] = xz;
259 trans->m[3][0] = xx * -x + xy * -y + xz * -z;
260 trans->m[0][1] = yx;
261 trans->m[1][1] = yy;
262 trans->m[2][1] = yz;
263 trans->m[3][1] = yx * -x + yy * -y + yz * -z;
264 trans->m[0][2] = zx;
265 trans->m[1][2] = zy;
266 trans->m[2][2] = zz;
267 trans->m[3][2] = zx * -x + zy * -y + zz * -z;
268 }
269
270 /* Function: al_invert_transform
271 */
al_invert_transform(ALLEGRO_TRANSFORM * trans)272 void al_invert_transform(ALLEGRO_TRANSFORM *trans)
273 {
274 float det, t;
275 ASSERT(trans);
276
277 det = trans->m[0][0] * trans->m[1][1] - trans->m[1][0] * trans->m[0][1];
278
279 t = trans->m[3][0];
280 trans->m[3][0] = ( trans->m[1][0] * trans->m[3][1] - t * trans->m[1][1]) / det;
281 trans->m[3][1] = (t * trans->m[0][1] - trans->m[0][0] * trans->m[3][1]) / det;
282
283 t = trans->m[0][0];
284 trans->m[0][0] = trans->m[1][1] / det;
285 trans->m[1][1] = t / det;
286
287 trans->m[0][1] = - trans->m[0][1] / det;
288 trans->m[1][0] = - trans->m[1][0] / det;
289 }
290
291 /* Function: al_transpose_transform
292 */
al_transpose_transform(ALLEGRO_TRANSFORM * trans)293 void al_transpose_transform(ALLEGRO_TRANSFORM *trans)
294 {
295 int i, j;
296 ASSERT(trans);
297
298 ALLEGRO_TRANSFORM t = *trans;
299 for (i = 0; i < 4; i++) {
300 for (j = 0; j < 4; j++) {
301 trans->m[i][j] = t.m[j][i];
302 }
303 }
304 }
305
306 /* Function: al_check_inverse
307 */
al_check_inverse(const ALLEGRO_TRANSFORM * trans,float tol)308 int al_check_inverse(const ALLEGRO_TRANSFORM *trans, float tol)
309 {
310 float det, norm, c0, c1, c3;
311 ASSERT(trans);
312
313 det = fabsf(trans->m[0][0] * trans->m[1][1] - trans->m[1][0] * trans->m[0][1]);
314 /*
315 We'll use the 1-norm, as it is the easiest to compute
316 */
317 c0 = fabsf(trans->m[0][0]) + fabsf(trans->m[0][1]);
318 c1 = fabsf(trans->m[1][0]) + fabsf(trans->m[1][1]);
319 c3 = fabsf(trans->m[3][0]) + fabsf(trans->m[3][1]) + 1;
320 norm = _ALLEGRO_MAX(_ALLEGRO_MAX(1, c0), _ALLEGRO_MAX(c1, c3));
321
322 return det > tol * norm;
323 }
324
325 /* Function: al_translate_transform
326 */
al_translate_transform(ALLEGRO_TRANSFORM * trans,float x,float y)327 void al_translate_transform(ALLEGRO_TRANSFORM *trans, float x, float y)
328 {
329 ASSERT(trans);
330
331 trans->m[3][0] += x;
332 trans->m[3][1] += y;
333 }
334
335
336 /* Function: al_translate_transform_3d
337 */
al_translate_transform_3d(ALLEGRO_TRANSFORM * trans,float x,float y,float z)338 void al_translate_transform_3d(ALLEGRO_TRANSFORM *trans, float x, float y,
339 float z)
340 {
341 ASSERT(trans);
342
343 trans->m[3][0] += x;
344 trans->m[3][1] += y;
345 trans->m[3][2] += z;
346 }
347
348
349 /* Function: al_rotate_transform
350 */
al_rotate_transform(ALLEGRO_TRANSFORM * trans,float theta)351 void al_rotate_transform(ALLEGRO_TRANSFORM *trans, float theta)
352 {
353 float c, s;
354 float t;
355 ASSERT(trans);
356
357 c = cosf(theta);
358 s = sinf(theta);
359
360 t = trans->m[0][0];
361 trans->m[0][0] = t * c - trans->m[0][1] * s;
362 trans->m[0][1] = t * s + trans->m[0][1] * c;
363
364 t = trans->m[1][0];
365 trans->m[1][0] = t * c - trans->m[1][1] * s;
366 trans->m[1][1] = t * s + trans->m[1][1] * c;
367
368 t = trans->m[3][0];
369 trans->m[3][0] = t * c - trans->m[3][1] * s;
370 trans->m[3][1] = t * s + trans->m[3][1] * c;
371 }
372
373 /* Function: al_scale_transform
374 */
al_scale_transform(ALLEGRO_TRANSFORM * trans,float sx,float sy)375 void al_scale_transform(ALLEGRO_TRANSFORM *trans, float sx, float sy)
376 {
377 ASSERT(trans);
378
379 trans->m[0][0] *= sx;
380 trans->m[0][1] *= sy;
381
382 trans->m[1][0] *= sx;
383 trans->m[1][1] *= sy;
384
385 trans->m[3][0] *= sx;
386 trans->m[3][1] *= sy;
387 }
388
389
390 /* Function: al_scale_transform_3d
391 */
al_scale_transform_3d(ALLEGRO_TRANSFORM * trans,float sx,float sy,float sz)392 void al_scale_transform_3d(ALLEGRO_TRANSFORM *trans, float sx, float sy,
393 float sz)
394 {
395 ASSERT(trans);
396
397 trans->m[0][0] *= sx;
398 trans->m[0][1] *= sy;
399 trans->m[0][2] *= sz;
400
401 trans->m[1][0] *= sx;
402 trans->m[1][1] *= sy;
403 trans->m[1][2] *= sz;
404
405 trans->m[2][0] *= sx;
406 trans->m[2][1] *= sy;
407 trans->m[2][2] *= sz;
408
409 trans->m[3][0] *= sx;
410 trans->m[3][1] *= sy;
411 trans->m[3][2] *= sz;
412 }
413
414 /* Function: al_transform_coordinates
415 */
al_transform_coordinates(const ALLEGRO_TRANSFORM * trans,float * x,float * y)416 void al_transform_coordinates(const ALLEGRO_TRANSFORM *trans, float *x, float *y)
417 {
418 float t;
419 ASSERT(trans);
420 ASSERT(x);
421 ASSERT(y);
422
423 t = *x;
424
425 *x = t * trans->m[0][0] + *y * trans->m[1][0] + trans->m[3][0];
426 *y = t * trans->m[0][1] + *y * trans->m[1][1] + trans->m[3][1];
427 }
428
429 /* Function: al_transform_coordinates_3d
430 */
al_transform_coordinates_3d(const ALLEGRO_TRANSFORM * trans,float * x,float * y,float * z)431 void al_transform_coordinates_3d(const ALLEGRO_TRANSFORM *trans,
432 float *x, float *y, float *z)
433 {
434 float rx, ry, rz;
435 ASSERT(trans);
436 ASSERT(x);
437 ASSERT(y);
438 ASSERT(z);
439
440 #define M(i, j) trans->m[i][j]
441
442 rx = M(0, 0) * *x + M(1, 0) * *y + M(2, 0) * *z + M(3, 0);
443 ry = M(0, 1) * *x + M(1, 1) * *y + M(2, 1) * *z + M(3, 1);
444 rz = M(0, 2) * *x + M(1, 2) * *y + M(2, 2) * *z + M(3, 2);
445
446 #undef M
447
448 *x = rx;
449 *y = ry;
450 *z = rz;
451 }
452
453 /* Function: al_transform_coordinates_4d
454 */
al_transform_coordinates_4d(const ALLEGRO_TRANSFORM * trans,float * x,float * y,float * z,float * w)455 void al_transform_coordinates_4d(const ALLEGRO_TRANSFORM *trans,
456 float *x, float *y, float *z, float *w)
457 {
458 float rx, ry, rz, rw;
459 ASSERT(trans);
460 ASSERT(x);
461 ASSERT(y);
462 ASSERT(z);
463 ASSERT(w);
464
465 #define M(i, j) trans->m[i][j]
466
467 rx = M(0, 0) * *x + M(1, 0) * *y + M(2, 0) * *z + M(3, 0) * *w;
468 ry = M(0, 1) * *x + M(1, 1) * *y + M(2, 1) * *z + M(3, 1) * *w;
469 rz = M(0, 2) * *x + M(1, 2) * *y + M(2, 2) * *z + M(3, 2) * *w;
470 rw = M(0, 3) * *x + M(1, 3) * *y + M(2, 3) * *z + M(3, 3) * *w;
471
472 #undef M
473
474 *x = rx;
475 *y = ry;
476 *z = rz;
477 *w = rw;
478 }
479
480 /* Function: al_transform_coordinates_3d_projective
481 */
al_transform_coordinates_3d_projective(const ALLEGRO_TRANSFORM * trans,float * x,float * y,float * z)482 void al_transform_coordinates_3d_projective(const ALLEGRO_TRANSFORM *trans,
483 float *x, float *y, float *z)
484 {
485 float w = 1;
486 al_transform_coordinates_4d(trans, x, y, z, &w);
487 *x /= w;
488 *y /= w;
489 *z /= w;
490 }
491
492 /* Function: al_compose_transform
493 */
al_compose_transform(ALLEGRO_TRANSFORM * trans,const ALLEGRO_TRANSFORM * other)494 void al_compose_transform(ALLEGRO_TRANSFORM *trans, const ALLEGRO_TRANSFORM *other)
495 {
496 #define E(x, y) \
497 (other->m[0][y] * trans->m[x][0] + \
498 other->m[1][y] * trans->m[x][1] + \
499 other->m[2][y] * trans->m[x][2] + \
500 other->m[3][y] * trans->m[x][3]) \
501
502 const ALLEGRO_TRANSFORM tmp = {{
503 { E(0, 0), E(0, 1), E(0, 2), E(0, 3) },
504 { E(1, 0), E(1, 1), E(1, 2), E(1, 3) },
505 { E(2, 0), E(2, 1), E(2, 2), E(2, 3) },
506 { E(3, 0), E(3, 1), E(3, 2), E(3, 3) }
507 }};
508
509 *trans = tmp;
510
511 #undef E
512 }
513
_al_transform_is_translation(const ALLEGRO_TRANSFORM * trans,float * dx,float * dy)514 bool _al_transform_is_translation(const ALLEGRO_TRANSFORM* trans,
515 float *dx, float *dy)
516 {
517 if (trans->m[0][0] == 1 &&
518 trans->m[1][0] == 0 &&
519 trans->m[2][0] == 0 &&
520 trans->m[0][1] == 0 &&
521 trans->m[1][1] == 1 &&
522 trans->m[2][1] == 0 &&
523 trans->m[0][2] == 0 &&
524 trans->m[1][2] == 0 &&
525 trans->m[2][2] == 1 &&
526 trans->m[3][2] == 0 &&
527 trans->m[0][3] == 0 &&
528 trans->m[1][3] == 0 &&
529 trans->m[2][3] == 0 &&
530 trans->m[3][3] == 1) {
531 *dx = trans->m[3][0];
532 *dy = trans->m[3][1];
533 return true;
534 }
535 return false;
536 }
537
538 /* Function: al_orthographic_transform
539 */
al_orthographic_transform(ALLEGRO_TRANSFORM * trans,float left,float top,float n,float right,float bottom,float f)540 void al_orthographic_transform(ALLEGRO_TRANSFORM *trans,
541 float left, float top, float n,
542 float right, float bottom, float f)
543 {
544 float delta_x = right - left;
545 float delta_y = top - bottom;
546 float delta_z = f - n;
547 ALLEGRO_TRANSFORM tmp;
548
549 al_identity_transform(&tmp);
550
551 tmp.m[0][0] = 2.0f / delta_x;
552 tmp.m[1][1] = 2.0f / delta_y;
553 tmp.m[2][2] = 2.0f / delta_z;
554 tmp.m[3][0] = -(right + left) / delta_x;
555 tmp.m[3][1] = -(top + bottom) / delta_y;
556 tmp.m[3][2] = -(f + n) / delta_z;
557 tmp.m[3][3] = 1.0f;
558
559 al_compose_transform(trans, &tmp);
560 }
561
562
563 /* Function: al_rotate_transform_3d
564 */
al_rotate_transform_3d(ALLEGRO_TRANSFORM * trans,float x,float y,float z,float angle)565 void al_rotate_transform_3d(ALLEGRO_TRANSFORM *trans,
566 float x, float y, float z, float angle)
567 {
568 double s = sin(angle);
569 double c = cos(angle);
570 double cc = 1 - c;
571 ALLEGRO_TRANSFORM tmp;
572
573 al_identity_transform(&tmp);
574
575 tmp.m[0][0] = (cc * x * x) + c;
576 tmp.m[0][1] = (cc * x * y) + (z * s);
577 tmp.m[0][2] = (cc * x * z) - (y * s);
578 tmp.m[0][3] = 0;
579
580 tmp.m[1][0] = (cc * x * y) - (z * s);
581 tmp.m[1][1] = (cc * y * y) + c;
582 tmp.m[1][2] = (cc * z * y) + (x * s);
583 tmp.m[1][3] = 0;
584
585 tmp.m[2][0] = (cc * x * z) + (y * s);
586 tmp.m[2][1] = (cc * y * z) - (x * s);
587 tmp.m[2][2] = (cc * z * z) + c;
588 tmp.m[2][3] = 0;
589
590 tmp.m[3][0] = 0;
591 tmp.m[3][1] = 0;
592 tmp.m[3][2] = 0;
593 tmp.m[3][3] = 1;
594
595 al_compose_transform(trans, &tmp);
596 }
597
598
599 /* Function: al_perspective_transform
600 */
al_perspective_transform(ALLEGRO_TRANSFORM * trans,float left,float top,float n,float right,float bottom,float f)601 void al_perspective_transform(ALLEGRO_TRANSFORM *trans,
602 float left, float top, float n,
603 float right, float bottom, float f)
604 {
605 float delta_x = right - left;
606 float delta_y = top - bottom;
607 float delta_z = f - n;
608 ALLEGRO_TRANSFORM tmp;
609
610 al_identity_transform(&tmp);
611
612 tmp.m[0][0] = 2.0f * n / delta_x;
613 tmp.m[1][1] = 2.0f * n / delta_y;
614 tmp.m[2][0] = (right + left) / delta_x;
615 tmp.m[2][1] = (top + bottom) / delta_y;
616 tmp.m[2][2] = -(f + n) / delta_z;
617 tmp.m[2][3] = -1.0f;
618 tmp.m[3][2] = -2.0f * f * n / delta_z;
619 tmp.m[3][3] = 0;
620
621 al_compose_transform(trans, &tmp);
622 }
623
624 /* Function: al_horizontal_shear_transform
625 */
al_horizontal_shear_transform(ALLEGRO_TRANSFORM * trans,float theta)626 void al_horizontal_shear_transform(ALLEGRO_TRANSFORM* trans, float theta)
627 {
628 float s;
629 ASSERT(trans);
630 s = -tanf(theta);
631
632 trans->m[0][0] += trans->m[0][1] * s;
633 trans->m[1][0] += trans->m[1][1] * s;
634 trans->m[3][0] += trans->m[3][1] * s;
635 }
636
637
638 /* Function: al_vertical_shear_transform
639 */
al_vertical_shear_transform(ALLEGRO_TRANSFORM * trans,float theta)640 void al_vertical_shear_transform(ALLEGRO_TRANSFORM* trans, float theta)
641 {
642 float s;
643 ASSERT(trans);
644 s = tanf(theta);
645
646 trans->m[0][1] += trans->m[0][0] * s;
647 trans->m[1][1] += trans->m[1][0] * s;
648 trans->m[3][1] += trans->m[3][0] * s;
649 }
650
651
652 /* vim: set sts=3 sw=3 et: */
653