Graphviz 13.0.0~dev.20250121.0651
Loading...
Searching...
No Matches
quad_prog_vpsc.c
Go to the documentation of this file.
1
17/**********************************************************
18 *
19 * Solve a quadratic function f(X) = X' e->A X + b X
20 * subject to a set of separation constraints e->cs
21 *
22 * Tim Dwyer, 2006
23 **********************************************************/
24
25#include <common/geomprocs.h>
26#include <neatogen/digcola.h>
27#include <stdbool.h>
28#include <util/alloc.h>
29#ifdef IPSEPCOLA
30#include <math.h>
31#include <stdlib.h>
32#include <time.h>
33#include <stdio.h>
34#include <float.h>
35#include <assert.h>
36#include <neatogen/matrix_ops.h>
37#include <neatogen/kkutils.h>
38#include <vpsc/csolve_VPSC.h>
41
42/* #define CONMAJ_LOGGING 1 */
43#define quad_prog_tol 1e-4
44
45/*
46 * Use gradient-projection to solve Variable Placement with Separation Constraints problem.
47 */
48int
49constrained_majorization_vpsc(CMajEnvVPSC * e, float *b, float *place,
50 int max_iterations)
51{
52 int i, j, counter;
53 float *g, *old_place, *d;
54 /* for laplacian computation need number of real vars and those
55 * dummy vars included in lap
56 */
57 int n = e->nv + e->nldv;
58 bool converged = false;
59#ifdef CONMAJ_LOGGING
60 static int call_no = 0;
61#endif /* CONMAJ_LOGGING */
62
63 if (max_iterations == 0)
64 return 0;
65 g = e->fArray1;
66 old_place = e->fArray2;
67 d = e->fArray3;
68 if (e->m > 0) {
69 for (i = 0; i < n; i++) {
70 setVariableDesiredPos(e->vs[i], place[i]);
71 }
72 satisfyVPSC(e->vpsc);
73 for (i = 0; i < n; i++) {
74 place[i] = getVariablePos(e->vs[i]);
75 }
76 }
77#ifdef CONMAJ_LOGGING
78 float prev_stress = 0;
79 for (i = 0; i < n; i++) {
80 prev_stress += 2 * b[i] * place[i];
81 for (j = 0; j < n; j++) {
82 prev_stress -= e->A[i][j] * place[j] * place[i];
83 }
84 }
85 FILE *logfile = fopen("constrained_majorization_log", "a");
86#endif
87
88 for (counter = 0; counter < max_iterations && !converged; counter++) {
89 float test = 0;
90 float alpha, beta;
91 float numerator = 0, denominator = 0, r;
92 converged = true;
93 /* find steepest descent direction */
94 for (i = 0; i < n; i++) {
95 old_place[i] = place[i];
96 g[i] = 2 * b[i];
97 for (j = 0; j < n; j++) {
98 g[i] -= 2 * e->A[i][j] * place[j];
99 }
100 }
101 for (i = 0; i < n; i++) {
102 numerator += g[i] * g[i];
103 r = 0;
104 for (j = 0; j < n; j++) {
105 r += 2 * e->A[i][j] * g[j];
106 }
107 denominator -= r * g[i];
108 }
109 if (denominator != 0)
110 alpha = numerator / denominator;
111 else
112 alpha = 1.0;
113 for (i = 0; i < n; i++) {
114 place[i] -= alpha * g[i];
115 }
116 if (e->m > 0) {
117 /* project to constraint boundary */
118 for (i = 0; i < n; i++) {
119 setVariableDesiredPos(e->vs[i], place[i]);
120 }
121 satisfyVPSC(e->vpsc);
122 for (i = 0; i < n; i++) {
123 place[i] = getVariablePos(e->vs[i]);
124 }
125 }
126 /* set place to the intersection of old_place-g and boundary and
127 * compute d, the vector from intersection pnt to projection pnt
128 */
129 for (i = 0; i < n; i++) {
130 d[i] = place[i] - old_place[i];
131 }
132 /* now compute beta */
133 numerator = 0, denominator = 0;
134 for (i = 0; i < n; i++) {
135 numerator += g[i] * d[i];
136 r = 0;
137 for (j = 0; j < n; j++) {
138 r += 2 * e->A[i][j] * d[j];
139 }
140 denominator += r * d[i];
141 }
142 if (denominator != 0.0)
143 beta = numerator / denominator;
144 else
145 beta = 1.0;
146
147 for (i = 0; i < n; i++) {
148 /* beta > 1.0 takes us back outside the feasible region
149 * beta < 0 clearly not useful and may happen due to numerical imp.
150 */
151 if (beta > 0 && beta < 1.0) {
152 place[i] = old_place[i] + beta * d[i];
153 }
154 test += fabsf(place[i] - old_place[i]);
155 }
156#ifdef CONMAJ_LOGGING
157 float stress = 0;
158 for (i = 0; i < n; i++) {
159 stress += 2 * b[i] * place[i];
160 for (j = 0; j < n; j++) {
161 stress -= e->A[i][j] * place[j] * place[i];
162 }
163 }
164 fprintf(logfile, "%d: stress=%f, test=%f, %s\n", call_no, stress,
165 test, (stress >= prev_stress) ? "No Improvement" : "");
166 prev_stress = stress;
167#endif
168 if (test > quad_prog_tol) {
169 converged = false;
170 }
171 }
172#ifdef CONMAJ_LOGGING
173 call_no++;
174 fclose(logfile);
175#endif
176 return counter;
177}
178
179/*
180 * Set up environment and global constraints (dir-edge constraints, containment constraints
181 * etc).
182 *
183 * diredges: 0=no dir edge constraints
184 * 1=one separation constraint for each edge (in acyclic subgraph)
185 * 2=DiG-CoLa level constraints
186 */
187CMajEnvVPSC *initCMajVPSC(int n, float *packedMat, vtx_data * graph,
188 ipsep_options * opt, int diredges)
189{
190 int i;
191 /* nv is the number of real nodes */
192 int nConCs;
193 CMajEnvVPSC *e = gv_alloc(sizeof(CMajEnvVPSC));
194 e->A = NULL;
195 /* if we have clusters then we'll need two constraints for each var in
196 * a cluster */
197 e->nldv = 2 * opt->clusters.nclusters;
198 e->nv = n - e->nldv;
199 e->ndv = 0;
200
201 e->gcs = NULL;
202 e->vs = gv_calloc(n, sizeof(Variable*));
203 for (i = 0; i < n; i++) {
204 e->vs[i] = newVariable(i, 1.0, 1.0);
205 }
206 e->gm = 0;
207 if (diredges == 1) {
208 if (Verbose)
209 fprintf(stderr, " generate edge constraints...\n");
210 for (i = 0; i < e->nv; i++) {
211 for (size_t j = 1; j < graph[i].nedges; j++) {
212 if (graph[i].edists[j] > 0.01) {
213 e->gm++;
214 }
215 }
216 }
217 e->gcs = newConstraints(e->gm);
218 e->gm = 0;
219 for (i = 0; i < e->nv; i++) {
220 for (size_t j = 1; j < graph[i].nedges; j++) {
221 int u = i, v = graph[i].edges[j];
222 if (graph[i].edists[j] > 0) {
223 e->gcs[e->gm++] =
224 newConstraint(e->vs[u], e->vs[v], opt->edge_gap);
225 }
226 }
227 }
228 } else if (diredges == 2) {
229 int *ordering = NULL, *ls = NULL, cvar;
230 double halfgap;
231 DigColaLevel *levels;
232 Variable **vs = e->vs;
233 /* e->ndv is the number of dummy variables required, one for each boundary */
234 if (compute_hierarchy(graph, e->nv, 1e-2, 1e-1, NULL, &ordering, &ls,
235 &e->ndv)) return NULL;
236 levels = assign_digcola_levels(ordering, e->nv, ls, e->ndv);
237 if (Verbose)
238 fprintf(stderr, "Found %d DiG-CoLa boundaries\n", e->ndv);
239 e->gm =
240 get_num_digcola_constraints(levels, e->ndv + 1) + e->ndv - 1;
241 e->gcs = newConstraints(e->gm);
242 e->gm = 0;
243 e->vs = gv_calloc(n + e->ndv, sizeof(Variable*));
244 for (i = 0; i < n; i++) {
245 e->vs[i] = vs[i];
246 }
247 free(vs);
248 /* create dummy vars */
249 for (i = 0; i < e->ndv; i++) {
250 /* dummy vars should have 0 weight */
251 cvar = n + i;
252 e->vs[cvar] = newVariable(cvar, 1.0, 0.000001);
253 }
254 halfgap = opt->edge_gap;
255 for (i = 0; i < e->ndv; i++) {
256 cvar = n + i;
257 /* outgoing constraints for each var in level below boundary */
258 for (int j = 0; j < levels[i].num_nodes; j++) {
259 e->gcs[e->gm++] =
260 newConstraint(e->vs[levels[i].nodes[j]], e->vs[cvar],
261 halfgap);
262 }
263 /* incoming constraints for each var in level above boundary */
264 for (int j = 0; j < levels[i + 1].num_nodes; j++) {
265 e->gcs[e->gm++] =
266 newConstraint(e->vs[cvar],
267 e->vs[levels[i + 1].nodes[j]], halfgap);
268 }
269 }
270 /* constraints between adjacent boundary dummy vars */
271 for (i = 0; i < e->ndv - 1; i++) {
272 e->gcs[e->gm++] =
273 newConstraint(e->vs[n + i], e->vs[n + i + 1], 0);
274 }
275 }
276 if (opt->clusters.nclusters > 0) {
277 Constraint **ecs = e->gcs;
278 nConCs = 2 * opt->clusters.nvars;
279 e->gcs = newConstraints(e->gm + nConCs);
280 for (i = 0; i < e->gm; i++) {
281 e->gcs[i] = ecs[i];
282 }
283 if (ecs != NULL)
284 deleteConstraints(0, ecs);
285 for (i = 0; i < opt->clusters.nclusters; i++) {
286 for (int j = 0; j < opt->clusters.clustersizes[i]; j++) {
287 Variable *v = e->vs[opt->clusters.clusters[i][j]];
288 Variable *cl = e->vs[e->nv + 2 * i];
289 Variable *cr = e->vs[e->nv + 2 * i + 1];
290 e->gcs[e->gm++] = newConstraint(cl, v, 0);
291 e->gcs[e->gm++] = newConstraint(v, cr, 0);
292 }
293 }
294 }
295
296 e->m = 0;
297 e->cs = NULL;
298 if (e->gm > 0) {
299 e->vpsc = newIncVPSC(n + e->ndv, e->vs, e->gm, e->gcs);
300 e->m = e->gm;
301 e->cs = e->gcs;
302 }
303 if (packedMat != NULL) {
304 e->A = unpackMatrix(packedMat, n);
305 }
306
307 e->fArray1 = gv_calloc(n, sizeof(float));
308 e->fArray2 = gv_calloc(n, sizeof(float));
309 e->fArray3 = gv_calloc(n, sizeof(float));
310 if (Verbose)
311 fprintf(stderr,
312 " initCMajVPSC done: %d global constraints generated.\n",
313 e->m);
314 return e;
315}
316
317void deleteCMajEnvVPSC(CMajEnvVPSC * e)
318{
319 int i;
320 if (e->A != NULL) {
321 free(e->A[0]);
322 free(e->A);
323 }
324 if (e->m > 0) {
325 deleteVPSC(e->vpsc);
326 if (e->cs != e->gcs && e->gcs != NULL)
327 deleteConstraints(0, e->gcs);
328 deleteConstraints(e->m, e->cs);
329 for (i = 0; i < e->nv + e->nldv + e->ndv; i++) {
330 deleteVariable(e->vs[i]);
331 }
332 free(e->vs);
333 }
334 free(e->fArray1);
335 free(e->fArray2);
336 free(e->fArray3);
337 free(e);
338}
339
340/* generate non-overlap constraints inside each cluster, including dummy
341 * nodes at bounds of cluster
342 * generate constraints again for top level nodes and clusters treating
343 * clusters as rectangles of dim (l,r,b,t)
344 * for each cluster map in-constraints to l out-constraints to r
345 *
346 * For now, we'll keep the global containment constraints already
347 * generated for each cluster, and simply generate non-overlap constraints
348 * for all nodes and then an additional set of non-overlap constraints for
349 * clusters that we'll map back to the dummy vars as above.
350 */
351void generateNonoverlapConstraints(CMajEnvVPSC * e,
352 float nsizeScale,
353 float **coords,
354 int k,
355 bool transitiveClosure,
356 ipsep_options * opt)
357{
358 Constraint **csol, **csolptr;
359 int i, j, mol = 0;
360 int n = e->nv + e->nldv;
361 boxf* bb = gv_calloc(n, sizeof(boxf));
362 bool genclusters = opt->clusters.nclusters > 0;
363 if (genclusters) {
364 /* n is the number of real variables, not dummy cluster vars */
365 n -= 2 * opt->clusters.nclusters;
366 }
367 if (k == 0) {
368 /* grow a bit in the x dimension, so that if overlap is resolved
369 * horizontally then it won't be considered overlapping vertically
370 */
371 nsizeScale *= 1.0001f;
372 }
373 for (i = 0; i < n; i++) {
374 bb[i].LL.x =
375 coords[0][i] - nsizeScale * opt->nsize[i].x / 2.0 -
376 opt->gap.x / 2.0;
377 bb[i].UR.x =
378 coords[0][i] + nsizeScale * opt->nsize[i].x / 2.0 +
379 opt->gap.x / 2.0;
380 bb[i].LL.y =
381 coords[1][i] - nsizeScale * opt->nsize[i].y / 2.0 -
382 opt->gap.y / 2.0;
383 bb[i].UR.y =
384 coords[1][i] + nsizeScale * opt->nsize[i].y / 2.0 +
385 opt->gap.y / 2.0;
386 }
387 if (genclusters) {
388 Constraint ***cscl = gv_calloc(opt->clusters.nclusters + 1,
389 sizeof(Constraint**));
390 int* cm = gv_calloc(opt->clusters.nclusters + 1, sizeof(int));
391 for (i = 0; i < opt->clusters.nclusters; i++) {
392 int cn = opt->clusters.clustersizes[i];
393 Variable** cvs = gv_calloc(cn + 2, sizeof(Variable*));
394 boxf* cbb = gv_calloc(cn + 2, sizeof(boxf));
395 /* compute cluster bounding bb */
396 boxf container;
397 container.LL.x = container.LL.y = DBL_MAX;
398 container.UR.x = container.UR.y = -DBL_MAX;
399 for (j = 0; j < cn; j++) {
400 int iv = opt->clusters.clusters[i][j];
401 cvs[j] = e->vs[iv];
402 B2BF(bb[iv], cbb[j]);
403 EXPANDBB(&container, bb[iv]);
404 }
405 B2BF(container, opt->clusters.bb[i]);
406 cvs[cn] = e->vs[n + 2 * i];
407 cvs[cn + 1] = e->vs[n + 2 * i + 1];
408 B2BF(container, cbb[cn]);
409 B2BF(container, cbb[cn + 1]);
410 if (k == 0) {
411 cbb[cn].UR.x = container.LL.x + 0.0001;
412 cbb[cn + 1].LL.x = container.UR.x - 0.0001;
413 cm[i] =
414 genXConstraints(cn + 2, cbb, cvs, &cscl[i],
415 transitiveClosure);
416 } else {
417 cbb[cn].UR.y = container.LL.y + 0.0001;
418 cbb[cn + 1].LL.y = container.UR.y - 0.0001;
419 cm[i] = genYConstraints(cn + 2, cbb, cvs, &cscl[i]);
420 }
421 mol += cm[i];
422 free (cvs);
423 free (cbb);
424 }
425 /* generate top level constraints */
426 {
427 int cn = opt->clusters.ntoplevel + opt->clusters.nclusters;
428 Variable** cvs = gv_calloc(cn, sizeof(Variable*));
429 boxf* cbb = gv_calloc(cn, sizeof(boxf));
430 for (i = 0; i < opt->clusters.ntoplevel; i++) {
431 int iv = opt->clusters.toplevel[i];
432 cvs[i] = e->vs[iv];
433 B2BF(bb[iv], cbb[i]);
434 }
435 /* make dummy variables for clusters */
436 for (i = opt->clusters.ntoplevel; i < cn; i++) {
437 cvs[i] = newVariable(123 + i, 1, 1);
438 j = i - opt->clusters.ntoplevel;
439 B2BF(opt->clusters.bb[j], cbb[i]);
440 }
441 i = opt->clusters.nclusters;
442 if (k == 0) {
443 cm[i] =
444 genXConstraints(cn, cbb, cvs, &cscl[i],
445 transitiveClosure);
446 } else {
447 cm[i] = genYConstraints(cn, cbb, cvs, &cscl[i]);
448 }
449 /* remap constraints from tmp dummy vars to cluster l and r vars */
450 for (i = opt->clusters.ntoplevel; i < cn; i++) {
451 double dgap;
452 j = i - opt->clusters.ntoplevel;
453 /* dgap is the change in required constraint gap.
454 * since we are going from a source rectangle the size
455 * of the cluster bounding box to a zero width (in x dim,
456 * zero height in y dim) rectangle, the change will be
457 * half the bb width.
458 */
459 if (k == 0) {
460 dgap = -(cbb[i].UR.x - cbb[i].LL.x) / 2.0;
461 } else {
462 dgap = -(cbb[i].UR.y - cbb[i].LL.y) / 2.0;
463 }
464 remapInConstraints(cvs[i], e->vs[n + 2 * j], dgap);
465 remapOutConstraints(cvs[i], e->vs[n + 2 * j + 1], dgap);
466 /* there may be problems with cycles between
467 * cluster non-overlap and diredge constraints,
468 * to resolve:
469 *
470 * for each constraint c:v->cvs[i]:
471 * if exists diredge constraint u->v where u in c:
472 * remap v->cl to cr->v (gap = height(v)/2)
473 *
474 * in = getInConstraints(cvs[i])
475 * for(c : in) {
476 * assert(c.right==cvs[i]);
477 * vin = getOutConstraints(v=c.left)
478 * for(d : vin) {
479 * if(d.left.cluster==i):
480 * tmp = d.left
481 * d.left = d.right
482 * d.right = tmp
483 * d.gap = height(d.right)/2
484 * }
485 * }
486 *
487 */
488 deleteVariable(cvs[i]);
489 }
490 mol += cm[opt->clusters.nclusters];
491 free (cvs);
492 free (cbb);
493 }
494 csolptr = csol = newConstraints(mol);
495 for (i = 0; i < opt->clusters.nclusters + 1; i++) {
496 /* copy constraints into csol */
497 for (j = 0; j < cm[i]; j++) {
498 *csolptr++ = cscl[i][j];
499 }
500 deleteConstraints(0, cscl[i]);
501 }
502 free (cscl);
503 free (cm);
504 } else {
505 if (k == 0) {
506 mol = genXConstraints(n, bb, e->vs, &csol, transitiveClosure);
507 } else {
508 mol = genYConstraints(n, bb, e->vs, &csol);
509 }
510 }
511 /* remove constraints from previous iteration */
512 if (e->m > 0) {
513 /* can't reuse instance of VPSC when constraints change! */
514 deleteVPSC(e->vpsc);
515 for (i = e->gm; i < e->m; i++) {
516 /* delete previous overlap constraints */
517 deleteConstraint(e->cs[i]);
518 }
519 /* just delete the array, not the elements */
520 if (e->cs != e->gcs)
521 deleteConstraints(0, e->cs);
522 }
523 /* if we have no global constraints then the overlap constraints
524 * are all we have to worry about.
525 * Otherwise, we have to copy the global and overlap constraints
526 * into the one array
527 */
528 if (e->gm == 0) {
529 e->m = mol;
530 e->cs = csol;
531 } else {
532 e->m = mol + e->gm;
533 e->cs = newConstraints(e->m);
534 for (i = 0; i < e->m; i++) {
535 if (i < e->gm) {
536 e->cs[i] = e->gcs[i];
537 } else {
538 e->cs[i] = csol[i - e->gm];
539 }
540 }
541 /* just delete the array, not the elements */
542 deleteConstraints(0, csol);
543 }
544 if (Verbose)
545 fprintf(stderr, " generated %d constraints\n", e->m);
546 e->vpsc = newIncVPSC(e->nv + e->nldv + e->ndv, e->vs, e->m, e->cs);
547 free (bb);
548}
549
550/*
551 * Statically remove overlaps, that is remove all overlaps by moving each node as
552 * little as possible.
553 */
554void removeoverlaps(int n, float **coords, ipsep_options * opt)
555{
556 int i;
557 CMajEnvVPSC *e = initCMajVPSC(n, NULL, NULL, opt, 0);
558 generateNonoverlapConstraints(e, 1.0, coords, 0, true, opt);
559 solveVPSC(e->vpsc);
560 for (i = 0; i < n; i++) {
561 coords[0][i] = getVariablePos(e->vs[i]);
562 }
563 generateNonoverlapConstraints(e, 1.0, coords, 1, false, opt);
564 solveVPSC(e->vpsc);
565 for (i = 0; i < n; i++) {
566 coords[1][i] = getVariablePos(e->vs[i]);
567 }
568 deleteCMajEnvVPSC(e);
569}
570
571/*
572 unpack the "ordering" array into an array of DigColaLevel
573*/
574DigColaLevel *assign_digcola_levels(int *ordering, int n, int *level_inds,
575 int num_divisions)
576{
577 int i, j;
578 DigColaLevel *l = gv_calloc(num_divisions + 1, sizeof(DigColaLevel));
579 /* first level */
580 l[0].num_nodes = level_inds[0];
581 l[0].nodes = gv_calloc(l[0].num_nodes, sizeof(int));
582 for (i = 0; i < l[0].num_nodes; i++) {
583 l[0].nodes[i] = ordering[i];
584 }
585 /* second through second last level */
586 for (i = 1; i < num_divisions; i++) {
587 l[i].num_nodes = level_inds[i] - level_inds[i - 1];
588 l[i].nodes = gv_calloc(l[i].num_nodes, sizeof(int));
589 for (j = 0; j < l[i].num_nodes; j++) {
590 l[i].nodes[j] = ordering[level_inds[i - 1] + j];
591 }
592 }
593 /* last level */
594 if (num_divisions > 0) {
595 l[num_divisions].num_nodes = n - level_inds[num_divisions - 1];
596 l[num_divisions].nodes = gv_calloc(l[num_divisions].num_nodes, sizeof(int));
597 for (i = 0; i < l[num_divisions].num_nodes; i++) {
598 l[num_divisions].nodes[i] =
599 ordering[level_inds[num_divisions - 1] + i];
600 }
601 }
602 return l;
603}
604
605/*********************
606get number of separation constraints based on the number of nodes in each level
607ie, num_sep_constraints = sum_i^{num_levels-1} (|L[i]|+|L[i+1]|)
608**********************/
609int get_num_digcola_constraints(DigColaLevel * levels, int num_levels)
610{
611 int i, nc = 0;
612 for (i = 1; i < num_levels; i++) {
613 nc += levels[i].num_nodes + levels[i - 1].num_nodes;
614 }
615 nc += levels[0].num_nodes + levels[num_levels - 1].num_nodes;
616 return nc;
617}
618
619#endif /* IPSEPCOLA */
Memory allocation wrappers that exit on failure.
static void * gv_calloc(size_t nmemb, size_t size)
Definition alloc.h:26
static void * gv_alloc(size_t size)
Definition alloc.h:47
void setVariableDesiredPos(Variable *v, double desiredPos)
Variable * newVariable(int id, double desiredPos, double weight)
Bridge for C programs to access solve_VPSC (which is in C++)
Constraint ** newConstraints(int m)
void deleteConstraint(Constraint *c)
VPSC * newIncVPSC(int n, Variable *vs[], int m, Constraint *cs[])
void deleteVariable(Variable *v)
void deleteConstraints(int m, Constraint **cs)
Constraint * newConstraint(Variable *left, Variable *right, double gap)
void remapInConstraints(Variable *u, Variable *v, double dgap)
int genXConstraints(int n, boxf *bb, Variable **vs, Constraint ***cs, bool transitiveClosure)
void remapOutConstraints(Variable *u, Variable *v, double dgap)
double getVariablePos(const Variable *v)
void satisfyVPSC(VPSC *vpsc)
int genYConstraints(int n, boxf *bb, Variable **vs, Constraint ***cs)
void deleteVPSC(VPSC *vpsc)
void solveVPSC(VPSC *vpsc)
#define B2BF(b, bf)
Definition geom.h:69
geometric functions (e.g. on points and boxes)
#define EXPANDBB(b0, b1)
Definition geomprocs.h:64
static bool Verbose
Definition gml2gv.c:23
void free(void *)
node NULL
Definition grammar.y:163
Agraph_t * graph(char *name)
Definition gv.cpp:30
#define alpha
Definition shapes.c:4058
A constraint determines a minimum or exact spacing required between two variables.
Definition constraint.h:25
Definition geom.h:41
pointf UR
Definition geom.h:41
pointf LL
Definition geom.h:41
double x
Definition geom.h:29
double y
Definition geom.h:29