File Coverage

blib/lib/Graph/Easy/Introspect.pm
Criterion Covered Total %
statement 446 472 94.4
branch 133 188 70.7
condition 142 256 55.4
subroutine 30 32 93.7
pod 0 18 0.0
total 751 966 77.7


line stmt bran cond sub pod time code
1              
2             package Graph::Easy::Introspect ;
3              
4 4     4   1233126 use strict ;
  4         8  
  4         179  
5 4     4   25 use warnings ;
  4         11  
  4         479  
6              
7             our $VERSION = '0.01' ;
8              
9 4         566 use Graph::Easy::Edge::Cell qw(
10             EDGE_HOR EDGE_VER EDGE_CROSS EDGE_HOLE
11             EDGE_N_E EDGE_N_W EDGE_S_E EDGE_S_W
12             EDGE_START_E EDGE_START_W EDGE_START_N EDGE_START_S
13             EDGE_END_E EDGE_END_W EDGE_END_N EDGE_END_S
14 4     4   36 ) ;
  4         8  
15              
16 4     4   31 use constant EDGE_TYPE_MASK => 0x000F ;
  4         14  
  4         433  
17 4     4   48 use constant EDGE_END_MASK => 0x00F0 ;
  4         9  
  4         248  
18 4     4   24 use constant EDGE_START_MASK => 0x0F00 ;
  4         8  
  4         290  
19 4     4   26 use constant EDGE_LABEL_CELL => 0x1000 ;
  4         9  
  4         803  
20              
21             my %CELL_TYPE_NAME =
22             (
23             EDGE_CROSS() => 'CROSS',
24             EDGE_HOR() => 'HOR',
25             EDGE_VER() => 'VER',
26             EDGE_N_E() => 'N_E',
27             EDGE_N_W() => 'N_W',
28             EDGE_S_E() => 'S_E',
29             EDGE_S_W() => 'S_W',
30             EDGE_HOLE() => 'HOLE',
31             7 => 'S_E_W',
32             8 => 'N_E_W',
33             9 => 'E_N_S',
34             10 => 'W_N_S',
35             12 => 'N_W_S',
36             13 => 'S_W_N',
37             14 => 'E_S_W',
38             15 => 'W_S_E',
39             ) ;
40              
41             our $WRAPPER_INSTALLED = 0 ;
42              
43             # ------------------------------------------------------------------------------
44              
45             sub install_layout_wrapper
46             {
47 31 100   31 0 157 return if $WRAPPER_INSTALLED ;
48              
49 4         2711 require Graph::Easy::Layout::Grid ;
50              
51             {
52 4     4   46 no warnings 'redefine' ;
  4         9  
  4         9187  
  4         6779  
53              
54 4         13 my $orig = \&Graph::Easy::_prepare_layout ;
55              
56             *Graph::Easy::_prepare_layout = sub
57             {
58 31     31   236364 my ($self, $format) = @_ ;
59              
60 31         179 my ($rows, $cols, $max_x, $max_y) = $orig->($self, $format) ;
61              
62 31 50 50     92260 if (($format // '') eq 'ascii' && !$self->{_introspect_captured})
      33        
63             {
64 31   50     103 my $align = eval { $self->attribute('align') } // 'left' ;
  31         117  
65 31         2877 my ($label) = eval { $self->_aligned_label($align) } ;
  31         132  
66 31   50     3287 $label //= [] ;
67 31   50     91 my $label_pos = eval { $self->attribute('graph', 'label-pos') } // 'top' ;
  31         89  
68              
69 31         2196 my $y_start = 0 ;
70 31         61 my $x_start = 0 ;
71              
72 31 50       127 if (@$label > 0)
73             {
74 0         0 unshift @$label, '' ;
75 0         0 push @$label, '' ;
76              
77 0 0       0 $y_start = scalar @$label if $label_pos eq 'top' ;
78              
79 0         0 my $old_max_x = $max_x ;
80              
81 0         0 for my $l (@$label)
82             {
83 0 0       0 $max_x = length($l) + 2 if length($l) > $max_x + 2 ;
84             }
85              
86 0         0 $x_start = int(($max_x - $old_max_x) / 2) ;
87             }
88              
89 31         62 my %char_pos ;
90              
91 31         56 for my $c (values %{$self->{cells}})
  31         154  
92             {
93             $char_pos{"$c->{x},$c->{y}"} =
94             {
95             char_x => ($cols->{$c->{x}} // 0) + $x_start,
96             char_y => ($rows->{$c->{y}} // 0) + $y_start,
97             render_w => $c->{w} // 0,
98 186 50 50     2801 render_h => $c->{h} // 0,
      50        
      50        
      50        
      50        
99             ref => ref($c),
100             name => ($c->can('name') ? ($c->name // '') : ''),
101             } ;
102             }
103              
104 31         369 $self->{_introspect_char_pos} = \%char_pos ;
105 31         118 $self->{_introspect_captured} = 1 ;
106             }
107              
108 31         173 return ($rows, $cols, $max_x, $max_y) ;
109 4         59 } ;
110             }
111              
112 4         11 $WRAPPER_INSTALLED = 1 ;
113             }
114              
115             # ------------------------------------------------------------------------------
116              
117             sub node_bbox
118             {
119 145     145 0 255 my ($node) = @_ ;
120              
121 145   50     315 my $x = $node->x // 0 ;
122 145   50     854 my $y = $node->y // 0 ;
123 145   50     857 my $cx = $node->{cx} // 1 ;
124 145   50     329 my $cy = $node->{cy} // 1 ;
125              
126 145         531 return ($x, $y, $x + $cx - 1, $y + $cy - 1) ;
127             }
128              
129             # ------------------------------------------------------------------------------
130              
131             sub port_side
132             {
133 70     70 0 137 my ($node, $port) = @_ ;
134              
135 70 100       158 return undef unless $port ;
136              
137 68         143 my ($x1, $y1, $x2, $y2) = node_bbox($node) ;
138 68         146 my ($px, $py) = @{$port}{qw/x y/} ;
  68         149  
139              
140 68 50 66     274 return 'right' if $px > $x2 && $py >= $y1 && $py <= $y2 ;
      66        
141 50 50 66     282 return 'left' if $px < $x1 && $py >= $y1 && $py <= $y2 ;
      66        
142 34 50 66     173 return 'bottom' if $py > $y2 && $px >= $x1 && $px <= $x2 ;
      66        
143 17 50 33     107 return 'top' if $py < $y1 && $px >= $x1 && $px <= $x2 ;
      33        
144              
145 0         0 return 'unknown' ;
146             }
147              
148             # ------------------------------------------------------------------------------
149              
150             sub cell_type_name
151             {
152 37     37 0 79 my ($type_code) = @_ ;
153              
154 37   33     578 return $CELL_TYPE_NAME{$type_code} // "UNKNOWN_$type_code" ;
155             }
156              
157             # ------------------------------------------------------------------------------
158              
159             sub extract_attrs
160             {
161 110     110 0 1518 my ($obj) = @_ ;
162              
163 110         213 my $att = $obj->{att} ;
164 110 50 33     587 return {} unless defined $att && ref $att eq 'HASH' ;
165              
166 110         199 my %attrs ;
167              
168 110         370 for my $key (sort keys %$att)
169             {
170 54         124 my $val = $att->{$key} ;
171 54 50       136 next unless defined $val ;
172 54 50       113 next if ref $val ;
173 54 100       129 next if $val eq '' ;
174 35         102 $attrs{$key} = $val ;
175             }
176              
177 110         1473 return \%attrs ;
178             }
179              
180             # ------------------------------------------------------------------------------
181              
182             sub extract_graph_attrs
183             {
184 31     31 0 148 my ($g) = @_ ;
185              
186 31         68 my $att = $g->{att} ;
187 31 50 33     212 return {} unless defined $att && ref $att eq 'HASH' ;
188              
189 31         100 my $graph_att = $att->{graph} ;
190 31 50 33     199 return {} unless defined $graph_att && ref $graph_att eq 'HASH' ;
191              
192 31         57 my %attrs ;
193              
194 31         129 for my $key (sort keys %$graph_att)
195             {
196 53         95 my $val = $graph_att->{$key} ;
197 53 50       107 next unless defined $val ;
198 53 50       99 next if ref $val ;
199 53 50       109 next if $val eq '' ;
200 53         125 $attrs{$key} = $val ;
201             }
202              
203 31         115 return \%attrs ;
204             }
205              
206             # ------------------------------------------------------------------------------
207              
208             sub compute_components
209             {
210 31     31 0 78 my ($g) = @_ ;
211              
212 31         53 my %adj ;
213              
214 31         87 for my $e ($g->edges)
215             {
216 35         472 my $f = $e->from->name ;
217 35         394 my $t = $e->to->name ;
218              
219 35         312 push @{$adj{$f}}, $t ;
  35         119  
220 35 100       117 push @{$adj{$t}}, $f unless $f eq $t ;
  34         112  
221             }
222              
223 31         86 my %component ;
224 31         92 my $comp_id = 0 ;
225              
226 31         96 for my $node (sort { $a->name cmp $b->name } $g->nodes)
  45         596  
227             {
228 69         370 my $name = $node->name ;
229 69 100       407 next if exists $component{$name} ;
230              
231 36         93 my @queue = ($name) ;
232              
233 36         97 while (@queue)
234             {
235 70         157 my $n = shift @queue ;
236 70 100       160 next if exists $component{$n} ;
237              
238 69         157 $component{$n} = $comp_id ;
239              
240 69 100       112 push @queue, grep { !exists $component{$_} } @{$adj{$n} || []} ;
  69         248  
  69         234  
241             }
242              
243 36         85 $comp_id++ ;
244             }
245              
246 31         213 return %component ;
247             }
248              
249             # ------------------------------------------------------------------------------
250              
251             sub sorted_nodes
252             {
253 31     31 0 121 my ($g) = @_ ;
254              
255 31         195 return sort { $a->name cmp $b->name } $g->nodes ;
  45         863  
256             }
257              
258             # ------------------------------------------------------------------------------
259              
260             sub sorted_edges
261             {
262 31     31 0 80 my ($g) = @_ ;
263              
264             return sort
265             {
266 31 50       121 $a->from->name cmp $b->from->name ||
  7         163  
267             $a->to->name cmp $b->to->name
268             } $g->edges ;
269             }
270              
271             # ------------------------------------------------------------------------------
272              
273             sub group_char_bbox
274             {
275 6     6 0 19 my ($g, $g_ast, $char_pos) = @_ ;
276              
277 6         19 my $bx1 = $g_ast->{bbox}{x1} ;
278 6         15 my $by1 = $g_ast->{bbox}{y1} ;
279 6         16 my $bx2 = $g_ast->{bbox}{x2} ;
280 6         47 my $by2 = $g_ast->{bbox}{y2} ;
281              
282 6         23 my $tl_key = ($bx1 - 1) . ',' . ($by1 - 1) ;
283 6         19 my $br_key = ($bx2 + 1) . ',' . ($by2 + 1) ;
284              
285 6         15 my $tl = $char_pos->{$tl_key} ;
286 6         14 my $br = $char_pos->{$br_key} ;
287              
288 6 50 33     30 return undef unless $tl && $br ;
289              
290             return
291             {
292             x => $tl->{char_x},
293             y => $tl->{char_y},
294             x2 => $br->{char_x} + $br->{render_w} - 1,
295             y2 => $br->{char_y} + $br->{render_h} - 1,
296             w => ($br->{char_x} + $br->{render_w} - 1) - $tl->{char_x} + 1,
297 6         70 h => ($br->{char_y} + $br->{render_h} - 1) - $tl->{char_y} + 1,
298             } ;
299             }
300              
301             # ------------------------------------------------------------------------------
302             # Compute the char-space face coordinates for an edge port attachment point.
303             # Returns the point ON the node face (border character row/col) at which the
304             # edge enters or leaves the node. This is distinct from the edge cell's
305             # top-left corner, which for short single-cell edges is the same cell for
306             # both from_port and to_port.
307              
308             sub face_char
309             {
310 136     136 0 244 my ($node_ast, $side) = @_ ;
311              
312 136         222 my $nx = $node_ast->{char_x} ;
313 136         209 my $ny = $node_ast->{char_y} ;
314 136         205 my $nw = $node_ast->{char_width} ;
315 136         206 my $nh = $node_ast->{char_height} ;
316              
317             # Use intrinsic width/height for the center calculation.
318             # Group membership expands char_width but the edge path uses the intrinsic
319             # node center, so int(intrinsic/2) gives the correct column/row offset.
320 136   33     288 my $iw = $node_ast->{width} || $nw ;
321 136   33     277 my $ih = $node_ast->{height} || $nh ;
322              
323 136 100       344 return ($nx + $nw, $ny + int($ih / 2)) if $side eq 'right' ;
324 100 100       263 return ($nx - 1, $ny + int($ih / 2)) if $side eq 'left' ;
325 68 100       200 return ($nx + int($iw / 2), $ny + $nh) if $side eq 'bottom' ;
326 34 50       124 return ($nx + int($iw / 2), $ny - 1) if $side eq 'top' ;
327              
328 0         0 return ($nx, $ny) ;
329             }
330              
331             # ------------------------------------------------------------------------------
332              
333             package Graph::Easy::Edge ;
334              
335 4     4   42 use strict ;
  4         9  
  4         207  
336 4     4   24 use warnings ;
  4         8  
  4         4171  
337              
338             sub path
339             {
340 0     0 0 0 my ($self) = @_ ;
341              
342 0 0       0 return [ map { [$_->{x}, $_->{y}] } @{$self->{cells} || []} ] ;
  0         0  
  0         0  
343             }
344              
345             # ------------------------------------------------------------------------------
346              
347             sub from_port
348             {
349 35     35 0 78 my ($self) = @_ ;
350              
351 35 100       87 return undef if $self->from->name eq $self->to->name ;
352              
353 34         561 my $from = $self->from ;
354 34         198 my ($x1, $y1, $x2, $y2) = Graph::Easy::Introspect::node_bbox($from) ;
355              
356             my @adjacent = grep
357             {
358 36         108 my ($cx, $cy) = ($_->{x}, $_->{y}) ;
359 36 100 33     464 ($cx > $x2 && $cy >= $y1 && $cy <= $y2) ||
      33        
      66        
      100        
      66        
      66        
      100        
      33        
      66        
      100        
360             ($cx < $x1 && $cy >= $y1 && $cy <= $y2) ||
361             ($cy > $y2 && $cx >= $x1 && $cx <= $x2) ||
362             ($cy < $y1 && $cx >= $x1 && $cx <= $x2)
363 34 50       73 } @{$self->{cells} || []} ;
  34         164  
364              
365 34 50       92 return undef unless @adjacent ;
366              
367 34         94 my $ncx = ($x1 + $x2) / 2 ;
368 34         75 my $ncy = ($y1 + $y2) / 2 ;
369              
370             my ($best) = sort
371             {
372 34         87 abs($a->{x} - $ncx) + abs($a->{y} - $ncy)
373             <=>
374 0         0 abs($b->{x} - $ncx) + abs($b->{y} - $ncy)
375             } @adjacent ;
376              
377 34         180 return { x => $best->{x}, y => $best->{y} } ;
378             }
379              
380             # ------------------------------------------------------------------------------
381              
382             sub to_port
383             {
384 35     35 0 74 my ($self) = @_ ;
385              
386 35 100       109 return undef if $self->from->name eq $self->to->name ;
387              
388 34         526 my $to = $self->to ;
389 34         185 my ($x1, $y1, $x2, $y2) = Graph::Easy::Introspect::node_bbox($to) ;
390              
391             my @adjacent = grep
392             {
393 36         91 my ($cx, $cy) = ($_->{x}, $_->{y}) ;
394 36 50 33     503 ($cx > $x2 && $cy >= $y1 && $cy <= $y2) ||
      66        
      100        
      66        
      66        
      66        
      100        
      33        
      33        
      66        
395             ($cx < $x1 && $cy >= $y1 && $cy <= $y2) ||
396             ($cy > $y2 && $cx >= $x1 && $cx <= $x2) ||
397             ($cy < $y1 && $cx >= $x1 && $cx <= $x2)
398 34 50       69 } @{$self->{cells} || []} ;
  34         163  
399              
400 34 50       127 return undef unless @adjacent ;
401              
402 34         72 my $ncx = ($x1 + $x2) / 2 ;
403 34         74 my $ncy = ($y1 + $y2) / 2 ;
404              
405             my ($best) = sort
406             {
407 34         70 abs($a->{x} - $ncx) + abs($a->{y} - $ncy)
408             <=>
409 0         0 abs($b->{x} - $ncx) + abs($b->{y} - $ncy)
410             } @adjacent ;
411              
412 34         154 return { x => $best->{x}, y => $best->{y} } ;
413             }
414              
415             # ------------------------------------------------------------------------------
416              
417             sub arrow_dir
418             {
419 35     35 0 73 my ($self) = @_ ;
420              
421 35 50       56 for my $c (@{$self->{cells} || []})
  35         152  
422             {
423 37   50     124 my $end = ($c->{type} // 0) & Graph::Easy::Introspect::EDGE_END_MASK ;
424              
425 37 100       133 return 'right' if $end == Graph::Easy::Introspect::EDGE_END_E ;
426 22 100       74 return 'left' if $end == Graph::Easy::Introspect::EDGE_END_W ;
427 19 100       65 return 'down' if $end == Graph::Easy::Introspect::EDGE_END_S ;
428 5 50       17 return 'up' if $end == Graph::Easy::Introspect::EDGE_END_N ;
429             }
430              
431 3         10 return undef ;
432             }
433              
434             # ------------------------------------------------------------------------------
435              
436             package Graph::Easy ;
437              
438 4     4   35 use strict ;
  4         37  
  4         206  
439 4     4   26 use warnings ;
  4         11  
  4         23524  
440              
441             sub ast
442             {
443 31     31 0 973484 my ($self) = @_ ;
444              
445 31         153 Graph::Easy::Introspect::install_layout_wrapper() ;
446              
447 31         144 delete $self->{_introspect_captured} ;
448 31         80 delete $self->{_introspect_char_pos} ;
449 31         65 delete $self->{_introspect_grid} ;
450 31         67 delete $self->{_introspect_cell_grid} ;
451              
452 31         61 my $ascii ;
453              
454 31         62 eval { $ascii = $self->as_ascii } ;
  31         148  
455              
456 31 50       129894 if ($@)
457             {
458             return
459             {
460 0         0 error => "$@",
461             meta => { introspect_version => $Graph::Easy::Introspect::VERSION },
462             } ;
463             }
464              
465 31   50     133 my $char_pos = $self->{_introspect_char_pos} || {} ;
466 31         165 my $grid = [ map { [split //, $_] } split /\n/, $ascii ] ;
  243         1514  
467              
468 31         122 my $total_height = scalar @$grid ;
469 31         107 my $total_width = 0 ;
470              
471 31         84 for my $row (@$grid)
472             {
473 243 100       560 $total_width = scalar @$row if scalar @$row > $total_width ;
474             }
475              
476 31         88 $self->{_introspect_grid} = $grid ;
477              
478 31         146 my @sorted_nodes = Graph::Easy::Introspect::sorted_nodes($self) ;
479 31         429 my @sorted_edges = Graph::Easy::Introspect::sorted_edges($self) ;
480              
481 31         512 my %node_index ;
482 31         54 my $ni = 0 ;
483              
484 31         74 for my $n (@sorted_nodes)
485             {
486 69         322 $node_index{$n->name} = $ni++ ;
487             }
488              
489 31         229 my %component = Graph::Easy::Introspect::compute_components($self) ;
490              
491 31         71 my %mult_groups ;
492              
493             {
494 31         55 my $eid = 0 ;
  31         65  
495              
496 31         127 for my $e (@sorted_edges)
497             {
498 35         104 my $key = $e->from->name . '|' . $e->to->name ;
499 35         513 push @{$mult_groups{$key}}, $eid++ ;
  35         139  
500             }
501             }
502              
503             my $is_directed = do
504 31         54 {
505 31         76 my $r = eval { $self->is_directed } ;
  31         150  
506 31 100       2795 defined $r ? ($r ? 1 : 0) : 1 ;
    50          
507             } ;
508              
509 31         119 my $graph_attrs = Graph::Easy::Introspect::extract_graph_attrs($self) ;
510 31 50 33     57 my $graph_label = do { my $l = eval { $self->label } ; (defined $l && $l ne '') ? $l : undef } ;
  31         55  
  31         119  
  31         442  
511 31   50     60 my $layout_algo = eval { $self->attribute('flow') } // 'default' ;
  31         97  
512              
513 31         2038 my @nodes_ast ;
514              
515 31         69 for my $node (@sorted_nodes)
516             {
517 69         219 my $name = $node->name ;
518 69   50     432 my $cell_x = $node->x // 0 ;
519 69   50     527 my $cell_y = $node->y // 0 ;
520 69   50     482 my $cx = $node->{cx} // 1 ;
521 69   50     215 my $cy = $node->{cy} // 1 ;
522 69         138 my $cell_key = "$cell_x,$cell_y" ;
523 69   50     214 my $cp = $char_pos->{$cell_key} // {} ;
524 69   50     180 my $char_x = $cp->{char_x} // 0 ;
525 69   50     200 my $char_y = $cp->{char_y} // 0 ;
526 69   0     186 my $char_w = $cp->{render_w} // ($node->width // 0) ;
      33        
527 69   0     157 my $char_h = $cp->{render_h} // ($node->height // 0) ;
      33        
528 69 50 33     106 my $label = do { my $l = eval { $node->label } ; (defined $l && $l ne '') ? $l : $name } ;
  69         107  
  69         181  
  69         13189  
529 69 50       127 my $is_anon = eval { $node->isa('Graph::Easy::Node::Anon') } ? 1 : 0 ;
  69         465  
530              
531             push @nodes_ast,
532             {
533             id => $name,
534             label => $label,
535             is_anon => $is_anon,
536             is_isolated => 0,
537             x => $cell_x,
538             y => $cell_y,
539             char_x => $char_x,
540             char_y => $char_y,
541             char_width => $char_w,
542             char_height => $char_h,
543             width => $node->width // 0,
544             height => $node->height // 0,
545             bbox =>
546             {
547             x1 => $cell_x,
548             y1 => $cell_y,
549             x2 => $cell_x + $cx - 1,
550             y2 => $cell_y + $cy - 1,
551             },
552 69   50     283 component => $component{$name} // 0,
      50        
      50        
553             groups => [],
554             edges_in => [],
555             edges_out => [],
556             ports =>
557             {
558             left => [],
559             right => [],
560             top => [],
561             bottom => [],
562             unknown => [],
563             },
564             attrs => Graph::Easy::Introspect::extract_attrs($node),
565             } ;
566             }
567              
568 31         55 my @edges_ast ;
569 31         60 my $edge_id = 0 ;
570              
571 31         104 for my $edge (@sorted_edges)
572             {
573 35         126 my $from = $edge->from ;
574 35         221 my $to = $edge->to ;
575              
576 35 100       210 my $is_self_loop = $from->name eq $to->name ? 1 : 0 ;
577             my $is_bidi = do
578 35         315 {
579 35         59 my $b = 0 ;
580 35 50       195 $b = 1 if ref($edge) =~ /Bidirectional/i ;
581 35 100 100     116 $b = 1 if !$b && (eval { $edge->bidirectional } // 0) ;
  35   66     112  
582 35         390 $b ;
583             } ;
584              
585 35         163 my $from_port = $edge->from_port ;
586 35         127 my $to_port = $edge->to_port ;
587 35         123 my $arrow_dir = $edge->arrow_dir ;
588              
589 35         111 my $from_side = Graph::Easy::Introspect::port_side($from, $from_port) ;
590 35         78 my $to_side = Graph::Easy::Introspect::port_side($to, $to_port) ;
591              
592 35         107 my $key = $from->name . '|' . $to->name ;
593 35 50       320 my $multiplicity = scalar @{$mult_groups{$key} || []} ;
  35         124  
594              
595 35 100 66     54 my $edge_label = do { my $l = eval { $edge->label } ; (defined $l && $l ne '') ? $l : undef } ;
  35         80  
  35         132  
  35         1767  
596              
597             # from_port and to_port char coords are ON the node face, not the edge cell.
598             # This gives correct distinct coords even for single-cell short edges.
599 35         123 my $from_ast_node = $nodes_ast[ $node_index{$from->name} ] ;
600 35         211 my $to_ast_node = $nodes_ast[ $node_index{$to->name} ] ;
601              
602             my $build_port = sub
603             {
604 70     70   441 my ($port, $node_ast, $side) = @_ ;
605 70 100       149 return undef unless $port ;
606              
607 68   50     171 my ($cx, $cy) = Graph::Easy::Introspect::face_char($node_ast, $side // 'unknown') ;
608              
609             return
610             {
611             x => $port->{x},
612             y => $port->{y},
613 68         407 char_x => $cx,
614             char_y => $cy,
615             } ;
616 35         328 } ;
617              
618 35         102 my ($label_x, $label_y, $label_char_x, $label_char_y) ;
619 35         0 my @path ;
620              
621 35 50       60 for my $c (@{$edge->{cells} || []})
  35         145  
622             {
623 37   50     111 my $type = $c->{type} // 0 ;
624 37         78 my $type_base = $type & Graph::Easy::Introspect::EDGE_TYPE_MASK ;
625 37 100       102 my $is_label = ($type & Graph::Easy::Introspect::EDGE_LABEL_CELL) ? 1 : 0 ;
626 37         98 my $ckey = "$c->{x},$c->{y}" ;
627 37   50     112 my $cp = $char_pos->{$ckey} // {} ;
628              
629 37 100 66     163 if ($is_label && !defined $label_x)
630             {
631 35         78 $label_x = $c->{x} ;
632 35         64 $label_y = $c->{y} ;
633 35         64 $label_char_x = $cp->{char_x} ;
634 35         65 $label_char_y = $cp->{char_y} ;
635             }
636              
637 37   50     91 my $cx = $cp->{char_x} // 0 ;
638 37   50     92 my $cy = $cp->{char_y} // 0 ;
639 37   50     173 my $cx2 = $cx + ($cp->{render_w} // 1) - 1 ;
640 37   50     116 my $cy2 = $cy + ($cp->{render_h} // 1) - 1 ;
641              
642             push @path,
643             {
644             x => $c->{x},
645             y => $c->{y},
646 37         182 char_x => $cx,
647             char_y => $cy,
648             char_x2 => $cx2,
649             char_y2 => $cy2,
650             line_x1 => 0,
651             line_y1 => 0,
652             line_x2 => 0,
653             line_y2 => 0,
654             type => Graph::Easy::Introspect::cell_type_name($type_base),
655             type_code => $type_base,
656             is_label => $is_label,
657             } ;
658             }
659              
660             # Sort path cells into traversal order by adjacency walk from from_port.
661             # $edge->{cells} is a hash so storage order is undefined.
662 35 100 66     268 if (!$is_self_loop && $from_port && @path > 1)
      100        
663             {
664 1         2 my %by_pos = map { my $k = "$_->{x},$_->{y}" ; $k => $_ } @path ;
  3         4  
  3         7  
665 1         3 my $start_key = "$from_port->{x},$from_port->{y}" ;
666              
667 1 50       4 unless (exists $by_pos{$start_key})
668             {
669 0         0 for my $d ([-1,0],[1,0],[0,-1],[0,1])
670             {
671 0         0 my $k = ($from_port->{x}+$d->[0]) . ',' . ($from_port->{y}+$d->[1]) ;
672 0 0       0 if (exists $by_pos{$k}) { $start_key = $k ; last }
  0         0  
  0         0  
673             }
674             }
675              
676 1         1 my @sorted ;
677             my %visited ;
678 1         2 my $cur = $start_key ;
679              
680 1   33     4 while (exists $by_pos{$cur} && !$visited{$cur})
681             {
682 3         5 $visited{$cur} = 1 ;
683 3         5 push @sorted, $by_pos{$cur} ;
684 3         3 my $c = $by_pos{$cur} ;
685 3         3 my $next ;
686              
687 3         8 for my $d ([-1,0],[1,0],[0,-1],[0,1])
688             {
689 12         17 my $nk = ($c->{x}+$d->[0]) . ',' . ($c->{y}+$d->[1]) ;
690 12 100 100     20 next unless exists $by_pos{$nk} && !$visited{$nk} ;
691 2         3 $next = $nk ;
692 2         3 last ;
693             }
694              
695 3 100       7 last unless defined $next ;
696 2         5 $cur = $next ;
697             }
698              
699 1 50       5 @path = @sorted if @sorted == @path ;
700             }
701              
702             # Compute line_* for each path cell using a waypoint-based polyline model.
703             #
704             # Waypoints: [from_port_char, corner_0_bend, ..., corner_n_bend, to_port_char]
705             # Each corner cell either introduces a straight run (next cell is VER/HOR)
706             # or terminates one (prev cell is VER/HOR).
707             # Introducing corner: assign wp[ci]->wp[ci+1], then advance ci.
708             # Terminating corner: advance ci first, then assign wp[ci]->wp[ci+1].
709             # VER/HOR cells: always assign wp[ci]->wp[ci+1], never advance ci.
710             #
711             # This gives contiguous, directed segments: each cell's endpoint equals
712             # the next cell's start point.
713              
714 35         257 my %is_straight_type = (VER => 1, HOR => 1, CROSS => 1, HOLE => 1) ;
715              
716 35         89 my ($fp_lx, $fp_ly) = (0, 0) ;
717 35         85 my ($tp_lx, $tp_ly) = (0, 0) ;
718              
719 35 100       90 unless ($is_self_loop)
720             {
721 34 50       146 ($fp_lx, $fp_ly) = Graph::Easy::Introspect::face_char($from_ast_node, $from_side)
722             if $from_side ne 'unknown' ;
723 34 50       114 ($tp_lx, $tp_ly) = Graph::Easy::Introspect::face_char($to_ast_node, $to_side)
724             if $to_side ne 'unknown' ;
725             }
726              
727             # Build mid_x/mid_y for each cell (needed for corner bends).
728 35         72 my %cell_mid ;
729 35         77 for my $p (@path)
730             {
731 37   50     151 my $cp2 = $char_pos->{"$p->{x},$p->{y}"} // {} ;
732 37   50     154 my $midx = ($cp2->{char_x} // 0) + int(($cp2->{render_w} // 1) / 2) ;
      50        
733 37   50     192 my $midy = ($cp2->{char_y} // 0) + int(($cp2->{render_h} // 1) / 2) ;
      50        
734 37         181 $cell_mid{"$p->{x},$p->{y}"} = [$midx, $midy] ;
735             }
736              
737             # Build waypoint list.
738 35         113 my @wp = ([$fp_lx, $fp_ly]) ;
739              
740 35         71 for my $p (@path)
741             {
742 37 100       136 next if $is_straight_type{$p->{type}} ;
743 3         8 my $m = $cell_mid{"$p->{x},$p->{y}"} ;
744 3         7 push @wp, [$m->[0], $m->[1]] ;
745             }
746              
747 35         86 push @wp, [$tp_lx, $tp_ly] ;
748              
749             # Assign line_* to each cell.
750 35         73 my $ci = 0 ;
751              
752 35         107 for my $i (0 .. $#path)
753             {
754 37         71 my $p = $path[$i] ;
755 37 100       134 my $prev = $i > 0 ? $path[$i-1] : undef ;
756              
757 37 100       113 if ($is_straight_type{$p->{type}})
758             {
759 34 100       123 my $next_ci = $ci + 1 < $#wp ? $ci + 1 : $#wp ;
760 34         82 $p->{line_x1} = $wp[$ci][0] ;
761 34         65 $p->{line_y1} = $wp[$ci][1] ;
762 34         124 $p->{line_x2} = $wp[$next_ci][0] ;
763 34         90 $p->{line_y2} = $wp[$next_ci][1] ;
764             }
765             else
766             {
767 3   66     8 my $prev_straight = $prev && $is_straight_type{$prev->{type}} ;
768              
769 3 100       23 $ci++ if $prev_straight ;
770              
771 3 100       16 my $next_ci = $ci + 1 < $#wp ? $ci + 1 : $#wp ;
772 3         7 $p->{line_x1} = $wp[$ci][0] ;
773 3         4 $p->{line_y1} = $wp[$ci][1] ;
774 3         6 $p->{line_x2} = $wp[$next_ci][0] ;
775 3         4 $p->{line_y2} = $wp[$next_ci][1] ;
776              
777 3         8 my $m = $cell_mid{"$p->{x},$p->{y}"} ;
778 3         5 $p->{bend_x} = $m->[0] ;
779 3         6 $p->{bend_y} = $m->[1] ;
780              
781 3 100       9 $ci++ unless $prev_straight ;
782             }
783             }
784              
785 35         134 push @edges_ast,
786             {
787             id => $edge_id,
788             from => $from->name,
789             to => $to->name,
790             is_self_loop => $is_self_loop,
791             is_bidirectional => $is_bidi,
792             multiplicity => $multiplicity,
793             arrow_dir => $arrow_dir,
794             from_port => $build_port->($from_port, $from_ast_node, $from_side),
795             to_port => $build_port->($to_port, $to_ast_node, $to_side),
796             from_side => $from_side,
797             to_side => $to_side,
798             label => $edge_label,
799             label_x => $label_x,
800             label_y => $label_y,
801             label_char_x => $label_char_x,
802             label_char_y => $label_char_y,
803             path => \@path,
804             attrs => Graph::Easy::Introspect::extract_attrs($edge),
805             } ;
806              
807 35         430 $edge_id++ ;
808             }
809              
810 31         67 my @groups_ast ;
811             my %group_index ;
812 31         54 my $gi = 0 ;
813              
814 31         52 my @graph_groups ;
815 31         60 eval { @graph_groups = $self->groups } ;
  31         139  
816              
817 31         298 for my $group (sort { $a->name cmp $b->name } @graph_groups)
  1         4  
818             {
819 6         35 my $gname = $group->name ;
820 6         40 my @gnodes = eval { map { $_->name } $group->nodes } ;
  6         33  
  9         135  
821              
822 6         46 $group_index{$gname} = $gi ;
823              
824 6         17 my ($bx1, $by1, $bx2, $by2) ;
825              
826 6         15 for my $nname (@gnodes)
827             {
828 9         36 my $n = $self->node($nname) ;
829 9 50       96 next unless $n ;
830              
831 9         40 my ($nx1, $ny1, $nx2, $ny2) = Graph::Easy::Introspect::node_bbox($n) ;
832              
833 9 100 66     39 $bx1 = $nx1 if !defined $bx1 || $nx1 < $bx1 ;
834 9 100 66     32 $by1 = $ny1 if !defined $by1 || $ny1 < $by1 ;
835 9 100 100     32 $bx2 = $nx2 if !defined $bx2 || $nx2 > $bx2 ;
836 9 100 100     34 $by2 = $ny2 if !defined $by2 || $ny2 > $by2 ;
837             }
838              
839 6   50     80 my $cell_bbox =
      50        
      50        
      50        
840             {
841             x1 => $bx1 // 0,
842             y1 => $by1 // 0,
843             x2 => $bx2 // 0,
844             y2 => $by2 // 0,
845             } ;
846              
847 6 50 33     13 my $glabel = do { my $l = eval { $group->label } ; (defined $l && $l ne '') ? $l : $gname } ;
  6         11  
  6         24  
  6         1315  
848              
849 6         48 my $gcb = Graph::Easy::Introspect::group_char_bbox($self, { bbox => $cell_bbox }, $char_pos) ;
850              
851             push @groups_ast,
852             {
853             id => $gname,
854             label => $glabel,
855             nodes => \@gnodes,
856             bbox => $cell_bbox,
857             char_x => $gcb ? $gcb->{x} : 0,
858             char_y => $gcb ? $gcb->{y} : 0,
859             char_width => $gcb ? $gcb->{w} : 0,
860 6 50       56 char_height => $gcb ? $gcb->{h} : 0,
    50          
    50          
    50          
861             attrs => Graph::Easy::Introspect::extract_attrs($group),
862             } ;
863              
864 6         26 $gi++ ;
865             }
866              
867 31         68 $edge_id = 0 ;
868              
869 31         76 for my $e_ast (@edges_ast)
870             {
871 35         100 my $from_idx = $node_index{$e_ast->{from}} ;
872 35         82 my $to_idx = $node_index{$e_ast->{to}} ;
873 35         59 my $eid = $e_ast->{id} ;
874              
875 35         89 push @{$nodes_ast[$from_idx]{edges_out}}, $eid ;
  35         104  
876 35         58 push @{$nodes_ast[$to_idx]{edges_in}}, $eid ;
  35         90  
877              
878 35 100       97 if ($e_ast->{from_port})
879             {
880 34   50     100 my $side = $e_ast->{from_side} || 'unknown' ;
881              
882 34         313 push @{$nodes_ast[$from_idx]{ports}{$side}},
883             {
884             edge_id => $eid,
885             role => 'out',
886             x => $e_ast->{from_port}{x},
887             y => $e_ast->{from_port}{y},
888             char_x => $e_ast->{from_port}{char_x},
889             char_y => $e_ast->{from_port}{char_y},
890 34         52 } ;
891             }
892              
893 35 100       117 if ($e_ast->{to_port})
894             {
895 34   50     120 my $side = $e_ast->{to_side} || 'unknown' ;
896              
897 34         288 push @{$nodes_ast[$to_idx]{ports}{$side}},
898             {
899             edge_id => $eid,
900             role => 'in',
901             x => $e_ast->{to_port}{x},
902             y => $e_ast->{to_port}{y},
903             char_x => $e_ast->{to_port}{char_x},
904             char_y => $e_ast->{to_port}{char_y},
905 34         95 } ;
906             }
907              
908 35         84 $edge_id++ ;
909             }
910              
911 31         60 for my $n_ast (@nodes_ast)
912             {
913 69         133 for my $side (qw/left right/)
914             {
915             $n_ast->{ports}{$side} =
916 138         216 [sort { $a->{y} <=> $b->{y} } @{$n_ast->{ports}{$side}}] ;
  0         0  
  138         420  
917             }
918              
919 69         120 for my $side (qw/top bottom/)
920             {
921             $n_ast->{ports}{$side} =
922 138         182 [sort { $a->{x} <=> $b->{x} } @{$n_ast->{ports}{$side}}] ;
  0         0  
  138         361  
923             }
924              
925 69         160 for my $side (qw/left right top bottom unknown/)
926             {
927 345         493 my $seq = 0 ;
928              
929 345         493 for my $p (@{$n_ast->{ports}{$side}})
  345         701  
930             {
931 68         188 $p->{seq} = $seq++ ;
932             }
933             }
934              
935             $n_ast->{is_isolated} =
936 69 100 100     99 (scalar(@{$n_ast->{edges_in}}) == 0 && scalar(@{$n_ast->{edges_out}}) == 0) ? 1 : 0 ;
937             }
938              
939 31         65 for my $g_ast (@groups_ast)
940             {
941 6         12 for my $nname (@{$g_ast->{nodes}})
  6         13  
942             {
943 9         22 my $idx = $node_index{$nname} ;
944 9 50       22 next unless defined $idx ;
945 9         17 push @{$nodes_ast[$idx]{groups}}, $g_ast->{id} ;
  9         36  
946             }
947             }
948              
949 31         63 my %cell_grid ;
950              
951 31         120 for my $key (keys %$char_pos)
952             {
953 186         320 my $cp = $char_pos->{$key} ;
954 186         284 my $type = 'unknown' ;
955              
956 186 100       808 if ($cp->{ref} =~ /::Node$/)
    100          
    50          
    0          
957             {
958 69         139 $type = 'node' ;
959             }
960             elsif ($cp->{ref} =~ /Edge/)
961             {
962 37         64 $type = 'edge' ;
963             }
964             elsif ($cp->{ref} =~ /Group/)
965             {
966 80         147 $type = 'group' ;
967             }
968             elsif ($cp->{ref} =~ /Node::Cell/)
969             {
970 0         0 $type = 'empty' ;
971             }
972              
973             $cell_grid{$key} =
974             {
975             type => $type,
976             name => $cp->{name},
977             char_x => $cp->{char_x},
978             char_y => $cp->{char_y},
979             render_w => $cp->{render_w},
980             render_h => $cp->{render_h},
981 186         1123 } ;
982             }
983              
984 31         115 $self->{_introspect_cell_grid} = \%cell_grid ;
985              
986             return
987             {
988 31         660 meta =>
989             {
990             introspect_version => $Graph::Easy::Introspect::VERSION,
991             graph_easy_version => $Graph::Easy::VERSION,
992             generated_at => time(),
993             layout_algorithm => $layout_algo,
994             },
995             graph =>
996             {
997             is_directed => $is_directed,
998             label => $graph_label,
999             total_width => $total_width,
1000             total_height => $total_height,
1001             attrs => $graph_attrs,
1002             },
1003             nodes => \@nodes_ast,
1004             edges => \@edges_ast,
1005             groups => \@groups_ast,
1006             } ;
1007             }
1008              
1009             # ------------------------------------------------------------------------------
1010              
1011             sub ast_grid
1012             {
1013 2     2 0 4447 my ($self) = @_ ;
1014              
1015 2         11 return $self->{_introspect_grid} ;
1016             }
1017              
1018             # ------------------------------------------------------------------------------
1019              
1020             sub ast_cell_grid
1021             {
1022 0     0 0   my ($self) = @_ ;
1023              
1024 0           return $self->{_introspect_cell_grid} ;
1025             }
1026              
1027             1 ;
1028              
1029             __END__