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.
- checksums.yaml +7 -0
- data/LICENSE.txt +17 -0
- data/bin/diagrammatron-edges +845 -0
- data/bin/diagrammatron-nodes +439 -0
- data/bin/diagrammatron-place +446 -0
- data/bin/diagrammatron-prune +117 -0
- data/bin/diagrammatron-render +355 -0
- data/bin/diagrammatron-template +107 -0
- data/bin/dot_json2diagrammatron +136 -0
- data/lib/common.rb +26 -0
- metadata +65 -0
@@ -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?
|