diagrammatron 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,845 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2021 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require '../lib/common.rb'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'set'
11
+ require 'pathname'
12
+
13
+
14
+ def work_copy(src, quiet)
15
+ work = { edges: {}, nodes: [] }
16
+ # Expected nodes, edges. Other pass-through.
17
+ label2idx = {}
18
+ errors = false
19
+ edge_nodes = Set.new
20
+ edges = src.fetch('edges', [])
21
+ unedges = []
22
+ selfedges = []
23
+ edges.each_index do |k|
24
+ edge = edges[k]
25
+ labels = edge.fetch('between', [])
26
+ if labels.nil? || labels.empty?
27
+ unedges.push(k)
28
+ elsif labels.size == 2
29
+ if labels.first == labels.last
30
+ selfedges.push(k)
31
+ else
32
+ edge_nodes.add labels.first
33
+ edge_nodes.add labels.last
34
+ work[:edges][k] = { idx: k, between: [ labels[0], labels[1] ] }
35
+ end
36
+ else
37
+ aargh "Edge #{k + 1} does not have two labels in 'between'"
38
+ errors = true
39
+ end
40
+ end
41
+ labeled_nodes = Set.new
42
+ unlabeled = []
43
+ nodes = src.fetch('nodes', [])
44
+ subsets = {}
45
+ nodes.each_index do |k|
46
+ node = nodes[k]
47
+ unless node.key? 'sid'
48
+ aargh "Node without sid: #{node.fetch('label', k + 1)}"
49
+ errors = true
50
+ next
51
+ end
52
+ unless node.key?('xo') && node.key?('yo')
53
+ aargh "Node without xo or yo: #{node.fetch('label', k + 1)}"
54
+ errors = true
55
+ next
56
+ end
57
+ sid = node['sid']
58
+ subsets[sid] = [] unless subsets.key? sid
59
+ subsets[sid].push(k)
60
+ work[:nodes].push({
61
+ idx: k,
62
+ sid: sid,
63
+ xo: node['xo'] * 2, # Make room for edge coordinates.
64
+ yo: node['yo'] * 2
65
+ })
66
+ unless node.key? 'label'
67
+ unlabeled.push k
68
+ next
69
+ end
70
+ label = node['label']
71
+ if label2idx.key?(label) && edge_nodes.key?(label)
72
+ aargh "Edge-referred label used twice: #{label}"
73
+ errors = true
74
+ end
75
+ label2idx[label] = k
76
+ labeled_nodes.add label
77
+ end
78
+ missing = edge_nodes - labeled_nodes
79
+ unless missing.empty?
80
+ aargh "Edges refer to missing node labels: #{missing.to_a.join(' ')}"
81
+ errors = true
82
+ end
83
+ return nil if errors
84
+ unused = labeled_nodes - edge_nodes
85
+ [ [ unused.to_a, 'unconnected labeled nodes' ],
86
+ [ unlabeled, 'unlabeled nodes' ],
87
+ [ selfedges, 'edges from node to itself' ],
88
+ [ unedges, 'edges without end-points' ]
89
+ ].each do |x|
90
+ next if quiet || x.first.empty?
91
+ aargh("Note, #{x.last}: #{x.first.join(' ')}")
92
+ end
93
+ work[:edges].each_value do |edge|
94
+ edge[:between][0] = label2idx[edge[:between][0]]
95
+ edge[:between][1] = label2idx[edge[:between][1]]
96
+ end
97
+ work[:subsets] = subsets
98
+ work
99
+ end
100
+
101
+ def edge_subsets(work)
102
+ subsets = {}
103
+ work[:edges].each_value do |edge|
104
+ s = work[:nodes][edge[:between].first][:sid]
105
+ subsets[s] = [] unless subsets.key? s
106
+ subsets[s].push(edge[:idx])
107
+ end
108
+ subsets
109
+ end
110
+
111
+ Segment = Struct.new(:vertical, :cc, :range, :edge_index, :at_node, :segment_index, :offset) do
112
+ def direction(s) # To decreasing or increasing coordinates.
113
+ return 0 if s.nil?
114
+ if segment_index < s.segment_index
115
+ (cc < s.range[1]) ? 1 : -1
116
+ else
117
+ (cc < s.range[0]) ? 1 : -1
118
+ end
119
+ end
120
+
121
+ def over_other_node(work, node_subset)
122
+ ck, rk = vertical ? [:xo, :yo] : [:yo, :xo]
123
+ i0, i1 = (range[0] < range[1]) ? [0, 1] : [1, 0]
124
+ node_subset.each do |n|
125
+ node = work[:nodes][n]
126
+ next unless cc == node[ck]
127
+ (0..1).each do |k|
128
+ return true if range[i0] < node[rk] && node[rk] < range[i1]
129
+ next if at_node[k]
130
+ return true if range[k] == node[rk]
131
+ end
132
+ end
133
+ false
134
+ end
135
+
136
+ def cross(s)
137
+ return false if vertical == s.vertical
138
+ return false if cc <= s.range.min || s.range.max <= cc
139
+ return false if s.cc <= range.min || range.max <= s.cc
140
+ true
141
+ end
142
+
143
+ def reverse(segment_idx)
144
+ Segment.new(vertical, cc, range.reverse, edge_index, at_node.reverse, segment_idx)
145
+ end
146
+
147
+ def length
148
+ (range[0] < range[1]) ? range[1] - range[0] : range[0] - range[1]
149
+ end
150
+
151
+ def range_overlap(s)
152
+ return false if range.max < s.range.min
153
+ return false if s.range.max < range.min
154
+ true
155
+ end
156
+
157
+ def increase?
158
+ range[0] < range[1]
159
+ end
160
+ end
161
+
162
+ def segment(x0, y0, x1, y1)
163
+ if x0 == x1
164
+ vert = true
165
+ cc = x0
166
+ range = [ y0, y1 ]
167
+ else
168
+ vert = false
169
+ cc = y0
170
+ range = [ x0, x1 ]
171
+ end
172
+ Segment.new(vert, cc, range, 0, [false, false], 0)
173
+ end
174
+
175
+ Connection = Struct.new(:node_index, :side_index) do
176
+ end
177
+
178
+ $paths = {}
179
+ Path = Struct.new(:edge_index, :ends, :segments, :id, :crosses, :steps) do
180
+ def push(s)
181
+ self.segments = [] if segments.nil?
182
+ s.edge_index = edge_index
183
+ s.segment_index = segments.size
184
+ segments.push s
185
+ self
186
+ end
187
+
188
+ def over_other_node(work, node_subset)
189
+ # Called after pushes so set end-point info here.
190
+ segments.first.at_node[0] = true
191
+ segments.last.at_node[1] = true
192
+ segments.each do |s|
193
+ return true if s.over_other_node(work, node_subset)
194
+ end
195
+ # Compute end-node side information.
196
+ dir, ck = segments.first.vertical ? [0, :yo] : [1, :xo]
197
+ n = work[:nodes][ends[0]]
198
+ dir += (segments.first.range[1] < n[ck]) ? 2 : 0
199
+ ends[0] = Connection.new(ends[0], dir)
200
+ dir, ck = segments.last.vertical ? [0, :yo] : [1, :xo]
201
+ n = work[:nodes][ends[1]]
202
+ dir += (segments.last.range[0] < n[ck]) ? 2 : 0
203
+ ends[1] = Connection.new(ends[1], dir)
204
+ self.id = $paths.size
205
+ self.crosses = Set.new
206
+ $paths[id] = self
207
+ false
208
+ end
209
+
210
+ def cross(p)
211
+ segments.each do |s|
212
+ p.segments.each do |t|
213
+ return true if s.cross(t)
214
+ end
215
+ end
216
+ false
217
+ end
218
+
219
+ def store_crossings(paths)
220
+ paths.each do |p|
221
+ next unless cross(p)
222
+ crosses.add p.id
223
+ p.crosses.add id
224
+ end
225
+ end
226
+
227
+ def segment_directions(index)
228
+ before = index.positive? ? segments[index - 1] : nil
229
+ after = (index + 1 < segments.size) ? segments[index + 1] : nil
230
+ s = segments[index]
231
+ if s.range[1] < s.range[0]
232
+ return [ s, s.direction(after), s.direction(before), before, after ]
233
+ end
234
+ [ s, s.direction(before), s.direction(after), before, after ]
235
+ end
236
+
237
+ def as_steps(conn)
238
+ if steps.nil?
239
+ v = []
240
+ (0...(segments.size)).each do |k|
241
+ s = segments[k]
242
+ v.push(s.length)
243
+ next if k + 1 == segments.size
244
+ mult = s.vertical ? -1 : 1
245
+ s1 = segments[k + 1]
246
+ if s.increase?
247
+ v.push(mult * (s1.increase? ? -1 : 1))
248
+ else
249
+ v.push(mult * (s1.increase? ? 1 : -1))
250
+ end
251
+ end
252
+ self.steps = v
253
+ else
254
+ v = steps
255
+ end
256
+ if conn == ends[1]
257
+ if steps.size > 1
258
+ v = steps.reverse
259
+ (1...v.size).step(2) do |k|
260
+ v[k] = -v[k]
261
+ end
262
+ end
263
+ end
264
+ v
265
+ end
266
+
267
+ def end_segment(conn)
268
+ (conn == ends[0]) ? segments.first : segments.last
269
+ end
270
+ end
271
+
272
+ def candidates(node0, node1, edge_index, work, node_subset)
273
+ n0 = work[:nodes][node0]
274
+ n1 = work[:nodes][node1]
275
+ cands = []
276
+ if n0[:xo] == n1[:xo]
277
+ # Vertical 3 alternatives.
278
+ [ -1, 0, 1 ].each do |offset|
279
+ path = Path.new(edge_index, [node0, node1])
280
+ path.push(segment(n0[:xo], n0[:yo], n0[:xo] + offset, n0[:yo])) unless offset.zero?
281
+ path.push segment(n0[:xo] + offset, n0[:yo], n0[:xo] + offset, n1[:yo])
282
+ path.push(segment(n0[:xo] + offset, n1[:yo], n1[:xo], n1[:yo])) unless offset.zero?
283
+ next if path.over_other_node(work, node_subset)
284
+ cands.push path
285
+ end
286
+ elsif n0[:yo] == n1[:yo]
287
+ # Horizontal 3 alternatives.
288
+ [ -1, 0, 1 ].each do |offset|
289
+ path = Path.new(edge_index, [node0, node1])
290
+ path.push(segment(n0[:xo], n0[:yo], n0[:xo], n0[:yo] + offset)) unless offset.zero?
291
+ path.push segment(n0[:xo], n0[:yo] + offset, n1[:xo], n0[:yo] + offset)
292
+ path.push(segment(n1[:xo], n0[:yo] + offset, n1[:xo], n1[:yo])) unless offset.zero?
293
+ next if path.over_other_node(work, node_subset)
294
+ cands.push path
295
+ end
296
+ else
297
+ # 9 offset combinations.
298
+ [ -1, 0, 1 ].each do |off0|
299
+ [ -1, 0, 1 ].each do |off1|
300
+ # Vertical-horizontal.
301
+ path = Path.new(edge_index, [node0, node1])
302
+ path.push(segment(n0[:xo], n0[:yo], n0[:xo] + off0, n0[:yo])) unless off0.zero?
303
+ path.push segment(n0[:xo] + off0, n0[:yo], n0[:xo] + off0, n1[:yo] + off1)
304
+ path.push segment(n0[:xo] + off0, n1[:yo] + off1, n1[:xo], n1[:yo] + off1)
305
+ path.push(segment(n1[:xo], n1[:yo] + off1, n1[:xo], n1[:yo])) unless off1.zero?
306
+ cands.push(path) unless path.over_other_node(work, node_subset)
307
+ # Horizontal-vertical.
308
+ path = Path.new(edge_index, [node0, node1])
309
+ path.push(segment(n0[:xo], n0[:yo], n0[:xo], n0[:yo] + off0)) unless off0.zero?
310
+ path.push segment(n0[:xo], n0[:yo] + off0, n1[:xo] + off1, n0[:yo] + off0)
311
+ path.push segment(n1[:xo] + off1, n0[:yo] + off0, n1[:xo] + off1, n1[:yo])
312
+ path.push(segment(n1[:xo] + off1, n1[:yo], n1[:xo], n1[:yo])) unless off1.zero?
313
+ cands.push(path) unless path.over_other_node(work, node_subset)
314
+ end
315
+ end
316
+ end
317
+ cands
318
+ end
319
+
320
+ def keep_direct_and_singletons(full)
321
+ full.each_pair do |edge_index, cands|
322
+ next if cands.size == 1
323
+ direct = nil
324
+ cands.each do |cand|
325
+ next if cand.segments.size != 1
326
+ direct = cand
327
+ break
328
+ end
329
+ next if direct.nil?
330
+ full[edge_index] = [ direct ]
331
+ end
332
+ end
333
+
334
+ def store_crossings(full)
335
+ idxs = full.keys.sort
336
+ (1...idxs.size).each do |k|
337
+ cands = full[idxs[k]]
338
+ (0...k).each do |n|
339
+ others = full[idxs[n]]
340
+ cands.each do |c|
341
+ c.store_crossings(others)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def keep_least_crossing(full)
348
+ # Compute crossing path pairs once as there will only be removals.
349
+ store_crossings(full)
350
+ loop do
351
+ # Find candidate with most crossings and most segments.
352
+ # Continue until each edge has only one candidate left.
353
+ chosen = nil
354
+ full.each_pair do |edge_index, cands|
355
+ next if cands.size == 1
356
+ cands.each_index do |k|
357
+ if chosen.nil?
358
+ chosen = [ full[edge_index][k], edge_index, k ]
359
+ next
360
+ end
361
+ c = cands[k]
362
+ next if c.crosses.size < chosen.first.crosses.size
363
+ next if c.crosses.size == chosen.first.crosses.size && c.segments.size <= chosen.first.segments.size
364
+ chosen = [ c, edge_index, k ]
365
+ end
366
+ end
367
+ break if chosen.nil?
368
+ chosen.first.crosses.each do |id|
369
+ $paths[id].crosses.delete(chosen.first.id)
370
+ end
371
+ $paths.delete chosen.first.id
372
+ full[chosen[1]].delete_at(chosen[2])
373
+ end
374
+ $paths = {}
375
+ chosen = {}
376
+ full.each_pair do |edge_index, cands|
377
+ chosen[edge_index] = cands[0]
378
+ end
379
+ chosen
380
+ end
381
+
382
+ def node_and_side_sets(chosen)
383
+ nodesides = {}
384
+ chosen.each_value do |path|
385
+ path.ends.each do |conn|
386
+ nodesides[conn] = nodesides.fetch(conn, []).push(path)
387
+ end
388
+ end
389
+ nodesides
390
+ end
391
+
392
+ def bend_order(ad, bd)
393
+ return ad[0] <=> bd[0] if ad.size == 1 && bd.size == 1
394
+ (1...[ad.size, bd.size].max).step(2) do |k|
395
+ d = ad.fetch(k, 0) <=> bd.fetch(k, 0)
396
+ return d unless d.zero?
397
+ m = ad.fetch(k, 0).positive? ? -1 : 1
398
+ d = ad.fetch(k - 1, 0) <=> bd.fetch(k - 1, 0)
399
+ return m * d unless d.zero?
400
+ end
401
+ 0
402
+ end
403
+
404
+ def path_order_at_side(a, b, conn)
405
+ d = bend_order(a.as_steps(conn), b.as_steps(conn))
406
+ return d unless d.zero?
407
+ # Paths in opposite directions and at smaller index node get reverse order.
408
+ d = a.edge_index <=> b.edge_index
409
+ if a.ends[0].node_index == b.ends[1].node_index && a.ends[1].node_index == b.ends[0].node_index
410
+ c = d.negative? ? a : b
411
+ if c.ends[0].node_index < c.ends[1].node_index && c.ends[0] == conn
412
+ return -d
413
+ end
414
+ end
415
+ d
416
+ end
417
+
418
+ def segment_order(a, b)
419
+ d = a[1] <=> b[1]
420
+ return d unless d.zero?
421
+ case a[1]
422
+ when 0 # Ascending on length, range minimum. =|
423
+ d = a[2].length <=> b[2].length
424
+ return d unless d.zero?
425
+ d = a[2].range.min <=> b[2].range.min
426
+ return d unless d.zero?
427
+ when 1 # From top left, down, bottom right: -|_
428
+ d = a[2].range.min <=> b[2].range.min # Guesstimate for DFS.
429
+ return d unless d.zero?
430
+ when 2 # From top right, down, bottom left: _|-
431
+ d = a[2].range.min <=> b[2].range.min # Guesstimate for DFS.
432
+ return -d unless d.zero?
433
+ when 3 # Descending on length, range maximum. |=
434
+ d = b[2].length <=> a[2].length
435
+ return d unless d.zero?
436
+ d = b[2].range.max <=> a[2].range.max
437
+ return d unless d.zero?
438
+ end
439
+ a[0].edge_index <=> b[0].edge_index
440
+ end
441
+
442
+ GapState = Struct.new(:order, :count) do
443
+ def fitness(segments, k)
444
+ crossings = 0
445
+ s = segments[k]
446
+ prev = (s[1] == 1) ? s[2].range.max : s[2].range.min
447
+ order.each do |n|
448
+ placed = segments[n]
449
+ if placed[2].range.min <= prev && prev <= placed[2].range.max
450
+ crossings += 1
451
+ end
452
+ v = (placed[1] == 1) ? placed[2].range.min : placed[2].range.max
453
+ if s[2].range.min <= v && v <= s[2].range.max
454
+ crossings += 1
455
+ end
456
+ end
457
+ count + crossings
458
+ end
459
+ end
460
+
461
+ def depth_first_search(segments, state, best)
462
+ if state.order.size < segments.size
463
+ segments.each_index do |k|
464
+ next if state.order.include? k
465
+ c = state.fitness(segments, k)
466
+ next if (best.nil? ? c + 1 : best.count) <= c
467
+ state.order.push(k)
468
+ best = depth_first_search(segments, GapState.new(state.order, c), best)
469
+ state.order.pop
470
+ end
471
+ else
472
+ best = GapState.new(state.order.clone, state.count)
473
+ end
474
+ best
475
+ end
476
+
477
+ def zigzag_order(segments)
478
+ return segments if segments.size < 2
479
+ # DFS. Fitness is how many end segments cross the placed segments.
480
+ # Sort so that those that cross least when at start are first and vice versa.
481
+ best = nil
482
+ segments.each_index do |k|
483
+ best = depth_first_search(segments, GapState.new([ k ], 0), best)
484
+ end
485
+ out = []
486
+ best.order.each do |k|
487
+ out.push segments[k]
488
+ end
489
+ out
490
+ end
491
+
492
+ def overlaps_set(c, others)
493
+ others.each do |sg|
494
+ return true if c.range_overlap(sg[2])
495
+ end
496
+ false
497
+ end
498
+
499
+ def group_minimal_offsets(group)
500
+ return 0 if group.empty?
501
+ off = { 1 => [] }
502
+ cands = off.keys
503
+ group.each do |a|
504
+ fit = false
505
+ cands.each do |k|
506
+ next if overlaps_set(a[2], off[k])
507
+ off[k].push(a)
508
+ fit = true
509
+ break
510
+ end
511
+ next if fit
512
+ cands.push(cands.last + 1)
513
+ off[cands.last] = [ a ]
514
+ end
515
+ off.each_pair do |offset, fitting|
516
+ fitting.each do |sg|
517
+ sg[0].offset = offset
518
+ end
519
+ end
520
+ off.keys.max
521
+ end
522
+
523
+ def group_stacked_offsets(group)
524
+ return 0 if group.empty?
525
+ group.each_index do |k|
526
+ group[k][0].offset = k + 1
527
+ end
528
+ return group.size
529
+ # This produces narrower gaps but they may be less clear.
530
+ prev = group[0]
531
+ prev[0].offset = 1
532
+ (1...group.size).each do |k|
533
+ g = group[k]
534
+ g[0].offset = prev[0].offset + (g[2].range_overlap(prev[2]) ? 1 : 0)
535
+ prev = g
536
+ end
537
+ prev[0].offset
538
+ end
539
+
540
+ def direct_range(paths)
541
+ [ paths.index { |p| p.segments.size == 1 }, paths.rindex { |p| p.segments.size == 1 }, paths.size ]
542
+ end
543
+
544
+ def offsets(conn, paths, direct_ranges)
545
+ d = direct_ranges.clone
546
+ here = d[conn]
547
+ d.delete(conn)
548
+ opposite = d.values.first
549
+ dlow = [ here[0], opposite[0] ].max
550
+ dhigh = [ here[2] - here[1], opposite[2] - opposite[1] ].max - 1
551
+ d = dlow + here[1] - here[0] + dhigh + 1
552
+ low, high, size = here
553
+ offsets = []
554
+ (0...low).each do |k|
555
+ offsets.push(Rational((k + 1) * dlow, (d + 1) * low))
556
+ end
557
+ (low..high).each do |k|
558
+ offsets.push(Rational(k + 1 - low + dlow, d + 1))
559
+ end
560
+ base = offsets.last
561
+ step = (1 - base) / (paths.size - high)
562
+ ((high + 1)...paths.size).each do |k|
563
+ offsets.push(base + (k - high) * step)
564
+ end
565
+ [ offsets, d ]
566
+ end
567
+
568
+ $insignificant = 0
569
+ def leg(a, b)
570
+ d = a - b
571
+ return 0 if d.abs < $insignificant
572
+ d.negative? ? -1 : 1
573
+ end
574
+
575
+ def surrounding_coordinates(nodesides, cc, conn, low, high)
576
+ nodesides[conn].each do |p|
577
+ c = p.end_segment(conn).offset
578
+ case leg(c, cc)
579
+ when -1
580
+ low = c if low < c
581
+ when 0 then next
582
+ when 1
583
+ high = c if c < high
584
+ end
585
+ end
586
+ [ low, high ]
587
+ end
588
+
589
+ def place_edges(work)
590
+ subsets = edge_subsets(work)
591
+ subsets.each_pair do |sid, subset|
592
+ full = Hash.new
593
+ subset.each do |edge_index|
594
+ link = work[:edges][edge_index][:between]
595
+ full[edge_index] = candidates(
596
+ link[0], link[1], edge_index, work, work[:subsets][sid])
597
+ end
598
+ keep_direct_and_singletons(full)
599
+ chosen = keep_least_crossing(full)
600
+ full = nil
601
+ # For each path, join it with node/side sub-set.
602
+ nodesides = node_and_side_sets(chosen)
603
+ nodesides.each_pair do |conn, paths|
604
+ paths.sort! { |a, b| path_order_at_side(a, b, conn) }
605
+ paths.reverse! if conn.side_index == 1 || conn.side_index == 2
606
+ end
607
+ # If set has any direct paths, they must align with the opposing group.
608
+ align_pairs = Set.new
609
+ splits = {}
610
+ nodesides.each_pair do |conn, paths|
611
+ splits[conn] = paths.size # Holds when no one-segment direct edges.
612
+ paths.each do |p|
613
+ next if p.segments.size > 1
614
+ align_pairs.add(
615
+ (p.ends[0].node_index < p.ends[1].node_index) ? p.ends : p.ends.reverse)
616
+ break
617
+ end
618
+ end
619
+ align_pairs.each do |pair|
620
+ count = {}
621
+ pair.each do |conn|
622
+ count[conn] = direct_range(nodesides[conn])
623
+ end
624
+ splits[pair[0]] = count
625
+ splits[pair[1]] = count
626
+ end
627
+ # End segment offsets.
628
+ nodesides.each_pair do |conn, paths|
629
+ d = splits[conn]
630
+ if d.is_a? Hash
631
+ off, d = offsets(conn, paths, d)
632
+ off.each_index do |k|
633
+ paths[k].end_segment(conn).offset = off[k]
634
+ end
635
+ splits[conn] = d
636
+ else # Simple case where the other end does not affect.
637
+ d += 1
638
+ paths.each_index do |k|
639
+ paths[k].end_segment(conn).offset = Rational(k + 1, d)
640
+ end
641
+ end
642
+ end
643
+ align_pairs = nil
644
+ $insignificant = 0.1 / (splits.values.max + 1)
645
+ splits = nil
646
+
647
+ gaps = {
648
+ false => {},
649
+ true => {}
650
+ }
651
+ chosen.each_value do |p|
652
+ (1...(p.segments.size - 1)).each do |k|
653
+ # Middle segments always have surrounding segments.
654
+ s, before, after, sb, sa = p.segment_directions(k)
655
+ so = s.clone # More accurate info on actual range with end offsets.
656
+ if so.range[0] < so.range[1]
657
+ so.range[0] += sb.offset unless sb.offset.nil?
658
+ so.range[1] += sa.offset.nil? ? 0.9999 : sa.offset
659
+ else
660
+ so.range[0] += sb.offset.nil? ? 0.9999 : sb.offset
661
+ so.range[1] += sa.offset unless sa.offset.nil?
662
+ end
663
+ group = (before.negative? ? 0 : 1) + (after.negative? ? 0 : 2)
664
+ d = gaps[s.vertical]
665
+ d[s.cc] = d.fetch(s.cc, []).push([ s, group, so ])
666
+ end
667
+ end
668
+ gaps.each_value do |direction|
669
+ direction.each_value do |gap|
670
+ gap.sort! { |a, b| segment_order(a, b) }
671
+ gleft = gap.select { |a| a[1].zero? }
672
+ gright = gap.select { |a| a[1] == 3 }
673
+ gmiddle = zigzag_order(gap.select { |a| a[1] == 1 || a[1] == 2 })
674
+ gmiddle.each do |s|
675
+ s[1] = 1
676
+ end
677
+ gap = gleft + gmiddle + gright
678
+ c = [
679
+ group_minimal_offsets(gleft),
680
+ group_stacked_offsets(gmiddle),
681
+ 0,
682
+ group_minimal_offsets(gright)
683
+ ]
684
+ before = [ 0 ]
685
+ denominator = 1 + c[0]
686
+ (1...c.size).each do |k|
687
+ denominator += c[k]
688
+ before[k] = c[k - 1] + before[k - 1]
689
+ end
690
+ gap.each do |sg|
691
+ sg[0].offset = c[sg[1]] + 1 - sg[0].offset if sg[1] > 1
692
+ sg[0].offset = Rational(sg[0].offset + before[sg[1]], denominator)
693
+ end
694
+ end
695
+ end
696
+ gaps = nil
697
+
698
+ # End segments in opposing sides may overlap if they extend enough.
699
+ overlapping = []
700
+ chosen.each_pair do |edge_index, path|
701
+ next if path.segments.size == 1
702
+ a = path.segments.first.clone
703
+ a.range[1] += path.segments[1].offset
704
+ b = path.segments.last.clone
705
+ b.range[0] += path.segments[path.segments.size - 2].offset
706
+ ac = a.cc + a.offset
707
+ bc = b.cc + b.offset
708
+ chosen.each_pair do |index, p|
709
+ next if index <= edge_index
710
+ next if p.segments.size == 1
711
+ c = p.segments.first.clone
712
+ c.range[1] += p.segments[1].offset
713
+ d = p.segments.last.clone
714
+ d.range[0] += p.segments[p.segments.size - 2].offset
715
+ cc = c.cc + c.offset
716
+ dc = d.cc + d.offset
717
+ if a.vertical == c.vertical && leg(ac, cc).zero? && a.range_overlap(c) && path.ends[0].node_index != p.ends[0].node_index
718
+ overlapping.push([ edge_index, path.ends[0], index, p.ends[0] ])
719
+ end
720
+ if b.vertical == c.vertical && leg(bc, cc).zero? && b.range_overlap(c) && path.ends[1].node_index != p.ends[0].node_index
721
+ overlapping.push([ edge_index, path.ends[1], index, p.ends[0] ])
722
+ end
723
+ if a.vertical == d.vertical && leg(ac, dc).zero? && a.range_overlap(d) && path.ends[0].node_index != p.ends[1].node_index
724
+ overlapping.push([ edge_index, path.ends[0], index, p.ends[1] ])
725
+ end
726
+ if b.vertical == d.vertical && leg(bc, dc).zero? && b.range_overlap(d) && path.ends[1].node_index != p.ends[1].node_index
727
+ overlapping.push([ edge_index, path.ends[1], index, p.ends[1] ])
728
+ end
729
+ end
730
+ end
731
+ # Place each overlapping pair offsets in range between closest coordinates.
732
+ overlapping.each do |s|
733
+ index_low, end_low, index_high, end_high = s
734
+ cc = chosen[index_low].end_segment(end_low).offset
735
+ low, high = surrounding_coordinates(nodesides, cc, end_low, 0, 1)
736
+ low, high = surrounding_coordinates(nodesides, cc, end_high, low, high)
737
+ r = high - low
738
+ chosen[index_low].end_segment(end_low).offset = low + 0.333333 * r
739
+ chosen[index_high].end_segment(end_high).offset = low + 0.666666 * r
740
+ end
741
+ nodesides = nil
742
+ overlapping = nil
743
+
744
+ # Convert each path to coordinate object array with offset added.
745
+ chosen.each_pair do |edge_index, path|
746
+ p = []
747
+ if path.segments.first.vertical
748
+ p.push({
749
+ 'xo' => path.segments.first.cc + path.segments.first.offset.to_f,
750
+ 'yo' => path.segments.first.range[0]
751
+ })
752
+ else
753
+ p.push({
754
+ 'xo' => path.segments.first.range[0],
755
+ 'yo' => path.segments.first.cc + path.segments.first.offset.to_f
756
+ })
757
+ end
758
+ (1...path.segments.size).each do |k|
759
+ s = path.segments[k]
760
+ if s.vertical
761
+ p.push({ 'xo' => s.cc + s.offset.to_f, 'yo' => p.last['yo'] })
762
+ else
763
+ p.push({ 'xo' => p.last['xo'], 'yo' => s.cc + s.offset.to_f })
764
+ end
765
+ end
766
+ if path.segments.last.vertical
767
+ p.push({ 'xo' => p.last['xo'], 'yo' => path.segments.last.range[1] })
768
+ else
769
+ p.push({ 'xo' => path.segments.last.range[1], 'yo' => p.last['yo'] })
770
+ end
771
+ work[:edges][edge_index][:path] = p
772
+ work[:edges][edge_index][:sid] = sid
773
+ end
774
+ end
775
+ end
776
+
777
+ def prepare_output(doc, work)
778
+ work[:nodes].each do |node|
779
+ doc['nodes'][node[:idx]]['xo'] = node[:xo]
780
+ doc['nodes'][node[:idx]]['yo'] = node[:yo]
781
+ end
782
+ work[:edges].each_value do |edge|
783
+ doc['edges'][edge[:idx]]['path'] = edge[:path]
784
+ doc['edges'][edge[:idx]]['sid'] = edge[:sid]
785
+ end
786
+ end
787
+
788
+ def main
789
+ input = nil
790
+ output = nil
791
+ quiet = false
792
+ parser = OptionParser.new do |opts|
793
+ opts.summary_indent = ' '
794
+ opts.summary_width = 20
795
+ opts.banner = 'Usage: diagrammatron-edges [options]'
796
+ opts.separator ''
797
+ opts.separator 'Options:'
798
+ opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
799
+ input = filename
800
+ end
801
+ opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
802
+ output = filename
803
+ end
804
+ opts.on('-q', '--quiet', 'No output except errors.') do
805
+ quiet = true
806
+ end
807
+ opts.on('-h', '--help', 'Print this help and exit.') do
808
+ $stdout.puts %(#{opts}
809
+
810
+ Input YAML file is expected to be the output of diagrammatron-nodes.
811
+ )
812
+ exit 0
813
+ end
814
+ end
815
+ parser.parse! ARGV
816
+
817
+ doc = load_source(input)
818
+ return 2 if doc.nil?
819
+
820
+ begin
821
+ work = work_copy(doc, quiet)
822
+ return 3 if work.nil?
823
+ rescue StandardError
824
+ return aargh('Error processing input.', 3)
825
+ end
826
+
827
+ place_edges(work)
828
+ prepare_output(doc, work)
829
+ begin
830
+ d = YAML.dump(doc, line_width: 1000000)
831
+ if output.nil?
832
+ $stdout.puts d
833
+ else
834
+ fp = Pathname.new output
835
+ fp.open('w') do |f|
836
+ f.puts d
837
+ end
838
+ end
839
+ rescue StandardError => e
840
+ return aargh("#{e}\nFailed to write output: #{output || 'stdout'}", 4)
841
+ end
842
+ 0
843
+ end
844
+
845
+ exit(main) if (defined? $unit_test).nil?