diagrammatron 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/bin/diagrammatron-edges +12 -91
- data/bin/diagrammatron-nodes +1 -1
- data/bin/diagrammatron-render +8 -4
- data/bin/diagrammatron-template +19 -6
- data/template/internal.yaml +1 -1
- data/template/svg_1.1.erb +15 -15
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 393c2881f870f2371e5af2e9a33fcfac3f82d9c3da69f7f377d87606b7230557
|
4
|
+
data.tar.gz: '019f726f35066af4fc209d61a2adbf36de1c6393d0089924912ca6c0e73baef0'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 737105dcbe2c8dc6d636833079d2313be3b5f2c66d86d252127664948365b92f976622b65019e9b5ddd1f7b760f35a15fef9e30fdd749fae99441a31fb03bb8e
|
7
|
+
data.tar.gz: c091a6a5f862353afb8f730dbf8b35cf226c5627e6d352f1ad8366ca6d03cb338e61264c22876948806afdc0965f1df0ee81223f07750f61c73a8cd171ab0538
|
data/LICENSE.txt
CHANGED
data/bin/diagrammatron-edges
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021
|
4
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
7
|
require_relative '../lib/common'
|
@@ -443,97 +443,11 @@ def segment_order(a, b)
|
|
443
443
|
a[0].edge_index <=> b[0].edge_index
|
444
444
|
end
|
445
445
|
|
446
|
-
GapState = Struct.new(:order, :cross_count, :lut) do
|
447
|
-
def fitness(segments, k)
|
448
|
-
table = lut[k]
|
449
|
-
crossings = 0
|
450
|
-
order.each do |n|
|
451
|
-
crossings += table[n]
|
452
|
-
end
|
453
|
-
cross_count + crossings
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
def candidate_compare(a, b)
|
458
|
-
d = a[1] <=> b[1]
|
459
|
-
return d unless d == 0
|
460
|
-
a[0] <=> b[0]
|
461
|
-
end
|
462
|
-
|
463
|
-
def candidate_order(lut, used)
|
464
|
-
# Compute crossing sums for all remaining segments among themselves.
|
465
|
-
cands = []
|
466
|
-
lut.keys.each do |k|
|
467
|
-
next if used.include? k
|
468
|
-
total = 0
|
469
|
-
lut.each_pair do |idx, v|
|
470
|
-
total += v[k] unless used.include? idx
|
471
|
-
end
|
472
|
-
cands.push([k, total])
|
473
|
-
end
|
474
|
-
cands.sort! { |a, b| candidate_compare(a, b) }
|
475
|
-
cands.map { |a| a[0] }
|
476
|
-
end
|
477
|
-
|
478
|
-
def depth_first_search(segments, state, best)
|
479
|
-
if state.order.size == segments.size
|
480
|
-
$stderr.puts state.order.join(', '), state.cross_count
|
481
|
-
return GapState.new(state.order.clone, state.cross_count, state.lut)
|
482
|
-
end
|
483
|
-
cands = candidate_order(state.lut, state.order)
|
484
|
-
cands.each do |k|
|
485
|
-
c = state.fitness(segments, k)
|
486
|
-
next if (best.nil? ? c + 1 : best.cross_count) <= c
|
487
|
-
state.order.push(k)
|
488
|
-
best = depth_first_search(segments, GapState.new(state.order, c, state.lut), best)
|
489
|
-
state.order.pop
|
490
|
-
end
|
491
|
-
best
|
492
|
-
end
|
493
|
-
|
494
446
|
def zigzag_order(from_right_up_to_left, from_left_up_to_right)
|
495
|
-
#
|
447
|
+
# Eventually interleave the two sets, if it makes sense.
|
496
448
|
out = []
|
497
449
|
from_right_up_to_left.each { |x| out.push x }
|
498
450
|
from_left_up_to_right.each { |x| out.push x }
|
499
|
-
return out
|
500
|
-
return segments if segments.size < 2
|
501
|
-
# DFS. Fitness is how many end segments cross the placed segments.
|
502
|
-
# Sort so that those that cross least when at start are first and vice versa.
|
503
|
-
lut = {}
|
504
|
-
segments.each_index do |k|
|
505
|
-
crossings = Hash.new(0)
|
506
|
-
lut[k] = crossings
|
507
|
-
s = segments[k]
|
508
|
-
prev = (s[1] == 1) ? s[2].range.max : s[2].range.min
|
509
|
-
segments.each_index do |n|
|
510
|
-
next if k == n
|
511
|
-
other = segments[n]
|
512
|
-
if other[2].range.min <= prev && prev <= other[2].range.max
|
513
|
-
crossings[n] = crossings[n] + 1
|
514
|
-
end
|
515
|
-
v = (other[1] == 1) ? other[2].range.min : other[2].range.max
|
516
|
-
if s[2].range.min <= v && v <= s[2].range.max
|
517
|
-
crossings[n] = crossings[n] + 1
|
518
|
-
end
|
519
|
-
end
|
520
|
-
end
|
521
|
-
$stderr.puts segments.size, lut
|
522
|
-
# Could take all crossings for k, sort, sum up to get minimum fitness for
|
523
|
-
# used set size N ignoring what set has. Since real fitness can not be
|
524
|
-
# lower, once minimal sum + cross_count exceeds best, we can not use that
|
525
|
-
# segment in any later position and can cut off search.
|
526
|
-
# Add to GapState after lut.
|
527
|
-
# Also, coult set time budget for single search and terminate search.
|
528
|
-
best = nil
|
529
|
-
cands = candidate_order(lut, [])
|
530
|
-
cands.each do |k|
|
531
|
-
best = depth_first_search(segments, GapState.new([ k ], 0, lut), best)
|
532
|
-
end
|
533
|
-
out = []
|
534
|
-
best.order.each do |k|
|
535
|
-
out.push segments[k]
|
536
|
-
end
|
537
451
|
out
|
538
452
|
end
|
539
453
|
|
@@ -717,10 +631,17 @@ def place_edges(work)
|
|
717
631
|
end
|
718
632
|
gaps.each_value do |direction|
|
719
633
|
direction.each_value do |gap|
|
720
|
-
gap.sort! { |a, b| segment_order(a, b) }
|
634
|
+
#gap.sort! { |a, b| segment_order(a, b) }
|
721
635
|
gleft = gap.select { |a| a[1].zero? }
|
636
|
+
gleft.sort! { |a, b| segment_order(a, b) }
|
722
637
|
gright = gap.select { |a| a[1] == 3 }
|
723
|
-
|
638
|
+
gright.sort! { |a, b| segment_order(a, b) }
|
639
|
+
gright.reverse!
|
640
|
+
grul = gap.select() { |a| a[1] == 1 }
|
641
|
+
grul.sort! { |a, b| segment_order(a, b) }
|
642
|
+
glur = gap.select() { |a| a[1] == 2 }
|
643
|
+
glur.sort! { |a, b| segment_order(a, b) }
|
644
|
+
gmiddle = zigzag_order(grul, glur)
|
724
645
|
gmiddle.each do |s|
|
725
646
|
s[1] = 1
|
726
647
|
end
|
@@ -735,7 +656,7 @@ def place_edges(work)
|
|
735
656
|
denominator = 1 + c[0]
|
736
657
|
(1...c.size).each do |k|
|
737
658
|
denominator += c[k]
|
738
|
-
before
|
659
|
+
before.push(c[k - 1] + before.last)
|
739
660
|
end
|
740
661
|
gap.each do |sg|
|
741
662
|
sg[0].offset = c[sg[1]] + 1 - sg[0].offset if sg[1] > 1
|
data/bin/diagrammatron-nodes
CHANGED
data/bin/diagrammatron-render
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021
|
4
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
7
|
require_relative '../lib/common'
|
@@ -78,8 +78,12 @@ end
|
|
78
78
|
|
79
79
|
class Styles
|
80
80
|
def base_styles(m, styles, group)
|
81
|
-
d = styles.dig(group, 'default')
|
82
|
-
|
81
|
+
d = styles.dig(group, 'default')
|
82
|
+
if d.nil?
|
83
|
+
d = m.fetch('default', {}) if d.nil?
|
84
|
+
else
|
85
|
+
m['default'] = m.fetch('default', {}).merge(d)
|
86
|
+
end
|
83
87
|
styles.fetch(group, {}).each_pair do |name, values|
|
84
88
|
s = d.clone
|
85
89
|
s.merge!(values) unless name == 'default'
|
@@ -107,7 +111,7 @@ class Styles
|
|
107
111
|
end
|
108
112
|
s.merge!(mapping['default']) unless found # Merge default at least.
|
109
113
|
# Keep values specified explicitly.
|
110
|
-
item.merge!(s) {|key, existing, from_template| existing }
|
114
|
+
item.merge!(s) { |key, existing, from_template| existing || from_template }
|
111
115
|
end
|
112
116
|
|
113
117
|
def apply_node_styles(node)
|
data/bin/diagrammatron-template
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021
|
4
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
7
|
require_relative '../lib/common'
|
@@ -21,11 +21,15 @@ def add_field(doc, field_name, content)
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def missing(doc)
|
24
|
-
%w[template].each do |key|
|
24
|
+
%w[template styles].each do |key|
|
25
25
|
next if doc.key? key
|
26
26
|
next if doc.key? "base64#{key}"
|
27
27
|
return aargh("#{key} is missing", 4)
|
28
28
|
end
|
29
|
+
%w[node edge diagram].each do |key|
|
30
|
+
v = doc.dig('styles', key, 'default')
|
31
|
+
return aargh("styles #{key} default is missing", 4) if v.nil?
|
32
|
+
end
|
29
33
|
nil
|
30
34
|
end
|
31
35
|
|
@@ -50,15 +54,24 @@ def main
|
|
50
54
|
Pairs all parameter field-names with content-files contents, starting with
|
51
55
|
either given root YAML file or with an empty root.
|
52
56
|
|
53
|
-
Any field name either in root document or in parameters is trusted to be
|
54
|
-
base-64 encoded without further checking.
|
55
|
-
|
56
57
|
Outputs a YAML-file that case be used with diagrammatron-render as a template.
|
57
58
|
All fields are base64-encoded for safety. diagrammatron-render will decode
|
58
59
|
them and rename the fields by removing the base64 prefix.
|
59
60
|
|
60
|
-
|
61
|
+
Root document is expected to have at least the following:
|
62
|
+
styles:
|
63
|
+
node:
|
64
|
+
defaults: {}
|
65
|
+
edge:
|
66
|
+
defaults: {}
|
67
|
+
diagram:
|
68
|
+
defaults: {}
|
69
|
+
|
70
|
+
Presence of "template" field is checked for.
|
61
71
|
Extra fields are not restricted in any manner.
|
72
|
+
|
73
|
+
Any field name in parameters starting with "base64" is trusted to be base-64
|
74
|
+
encoded without further checking.
|
62
75
|
)
|
63
76
|
exit 0
|
64
77
|
end
|
data/template/internal.yaml
CHANGED
@@ -29,4 +29,4 @@ styles:
|
|
29
29
|
default:
|
30
30
|
stroke_width: 2
|
31
31
|
stroke: "#000000"
|
32
|
-
base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+
|
32
|
+
base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpoaCArPSAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnaGVpZ2h0X21hcmdpbicpCgpvdXQgPSBbCiAgJSg8c3ZnIHdpZHRoPSIje3cgKyAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnd2lkdGhfbWFyZ2luJyl9IiBoZWlnaHQ9IiN7aGh9IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiPikKXQokcmVuZGVyLmRvYy5mZXRjaCgnZWRnZXMnLCBbXSkuZWFjaCBkbyB8ZWRnZXwKICBsaW5lc3R5bGUgPSAlKGZpbGw9Im5vbmUiIHN0cm9rZT0iI3tlZGdlWydzdHJva2UnXX0iIHN0cm9rZS13aWR0aD0iI3tlZGdlWydzdHJva2Vfd2lkdGgnXX0iKQogIHBhdGggPSBlZGdlLmZldGNoKCdwYXRoJywgbmlsKQogIG5leHQgaWYgcGF0aC5uaWw/CiAgcGF0aC5lYWNoIGRvIHxwfAogICAgcFsneG8nXSA9IHBbJ3hvJ10udG9faS50b19zCiAgICBwWyd5byddID0gKGhoIC0gcFsneW8nXSkudG9faS50b19zCiAgZW5kCiAgaWYgcGF0aC5zaXplID09IDIKICAgIG91dC5wdXNoKCUoPGxpbmUgI3tsaW5lc3R5bGV9IHgxPSIje3BhdGhbMF1bJ3hvJ119IiB4Mj0iI3twYXRoWzFdWyd4byddfSIgeTE9IiN7cGF0aFswXVsneW8nXX0iIHkyPSIje3BhdGhbMV1bJ3lvJ119Ii8+KSkKICBlbHNlCiAgICBwdHMgPSBwYXRoLm1hcCB7IHxwfCAiI3twWyd4byddfSwje3BbJ3lvJ119IiB9CiAgICBvdXQucHVzaCglKDxwb2x5bGluZSAje2xpbmVzdHlsZX0gcG9pbnRzPSIje3B0cy5qb2luKCcgJyl9Ii8+KSkKICBlbmQKZW5kCiRyZW5kZXIuZG9jLmZldGNoKCdub2RlcycsIFtdKS5lYWNoIGRvIHxub2RlfAogIHcgPSBub2RlWyd3J10udG9faQogIGggPSBub2RlWydoJ10udG9faQogIHggPSBub2RlWyd4byddLnRvX2kKICB5ID0gaGggLSBub2RlWyd5byddLnRvX2kgLSBoCiAgbm9kZXN0eWxlID0gJShmaWxsPSIje25vZGVbJ2ZpbGwnXX0iIHN0cm9rZT0iI3tub2RlWydzdHJva2UnXX0iIHN0cm9rZS13aWR0aD0iI3tub2RlWydzdHJva2Vfd2lkdGgnXX0iKQogIG91dC5wdXNoKCUoPHJlY3QgI3tub2Rlc3R5bGV9IGhlaWdodD0iI3tofSIgd2lkdGg9IiN7d30iIHg9IiN7eH0iIHk9IiN7eX0iLz4pKQogIHggKz0gbm9kZVsnd2lkdGhfbWFyZ2luJ10KICBmcyA9IG5vZGVbJ2ZvbnRfc2l6ZSddCiAgbGggPSBmcyAqICgxICsgbm9kZVsnZm9udF9saW5lX3NwYWNpbmcnXSkKICB5ICs9IG5vZGVbJ2hlaWdodF9tYXJnaW4nXSArIGZzICogbm9kZVsnZm9udF9hc2NlbmQnXSAjIEJhc2VsaW5lIGZvciBmaXJzdCBsaW5lLgogIHVybCA9IG5vZGUuZmV0Y2goJ3VybCcsIG5pbCkKICB1cmwuZW5jb2RlISg6eG1sID0+IDphdHRyKSB1bmxlc3MgdXJsLm5pbD8KICB5MCA9IHkKICB0ZXh0c3R5bGUgPSAlKGZpbGw9IiN7bm9kZVsnZm9udF9maWxsJ119IiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iI3tub2RlWydmb250X2ZpbGwnXX0iIHN0cm9rZS13aWR0aD0iMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIpCiAgbGlua3N0eWxlID0gJShmaWxsPSIje25vZGVbJ3VybF9maWxsJ119IiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iI3tub2RlWyd1cmxfZmlsbCddfSIgc3Ryb2tlLXdpZHRoPSIwIiB4bWw6c3BhY2U9InByZXNlcnZlIikKICBub2RlWyd0ZXh0J10uZWFjaCBkbyB8bGluZXwKICAgIGxpbmUuZW5jb2RlISg6eG1sID0+IDp0ZXh0KQogICAgaWYgdXJsLm5pbD8KICAgICAgb3V0LnB1c2goJSg8dGV4dCAje3RleHRzdHlsZX0geD0iI3t4fSIgeT0iI3t5MH0iPiN7bGluZX08L3RleHQ+KSkKICAgIGVsc2UKICAgICAgb3V0LnB1c2goJSg8YSB4bGluazpocmVmPSN7dXJsfSB0YXJnZXQ9Il9wYXJlbnQiPjx0ZXh0ICN7bGlua3N0eWxlfSB4PSIje3h9IiB5PSIje3kwfSI+I3tsaW5lfTwvdGV4dD48L2E+KSkKICAgIGVuZAogICAgeTAgKz0gbGggIyBTaGlmdCBiYXNlbGluZSBieSBmdWxsIGxpbmUgKyBzcGFjaW5nIGhlaWdodC4KICBlbmQKZW5kCm91dC5qb2luKCJcbiIpCiU+Cjwvc3ZnPgo=
|
data/template/svg_1.1.erb
CHANGED
@@ -6,6 +6,21 @@ hh += $render.doc.dig('diagram', 'height_margin')
|
|
6
6
|
out = [
|
7
7
|
%(<svg width="#{w + $render.doc.dig('diagram', 'width_margin')}" height="#{hh}" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">)
|
8
8
|
]
|
9
|
+
$render.doc.fetch('edges', []).each do |edge|
|
10
|
+
linestyle = %(fill="none" stroke="#{edge['stroke']}" stroke-width="#{edge['stroke_width']}")
|
11
|
+
path = edge.fetch('path', nil)
|
12
|
+
next if path.nil?
|
13
|
+
path.each do |p|
|
14
|
+
p['xo'] = p['xo'].to_i.to_s
|
15
|
+
p['yo'] = (hh - p['yo']).to_i.to_s
|
16
|
+
end
|
17
|
+
if path.size == 2
|
18
|
+
out.push(%(<line #{linestyle} x1="#{path[0]['xo']}" x2="#{path[1]['xo']}" y1="#{path[0]['yo']}" y2="#{path[1]['yo']}"/>))
|
19
|
+
else
|
20
|
+
pts = path.map { |p| "#{p['xo']},#{p['yo']}" }
|
21
|
+
out.push(%(<polyline #{linestyle} points="#{pts.join(' ')}"/>))
|
22
|
+
end
|
23
|
+
end
|
9
24
|
$render.doc.fetch('nodes', []).each do |node|
|
10
25
|
w = node['w'].to_i
|
11
26
|
h = node['h'].to_i
|
@@ -32,21 +47,6 @@ $render.doc.fetch('nodes', []).each do |node|
|
|
32
47
|
y0 += lh # Shift baseline by full line + spacing height.
|
33
48
|
end
|
34
49
|
end
|
35
|
-
$render.doc.fetch('edges', []).each do |edge|
|
36
|
-
linestyle = %(fill="none" stroke="#{edge['stroke']}" stroke-width="#{edge['stroke_width']}")
|
37
|
-
path = edge.fetch('path', nil)
|
38
|
-
next if path.nil?
|
39
|
-
path.each do |p|
|
40
|
-
p['xo'] = p['xo'].to_i.to_s
|
41
|
-
p['yo'] = (hh - p['yo']).to_i.to_s
|
42
|
-
end
|
43
|
-
if path.size == 2
|
44
|
-
out.push(%(<line #{linestyle} x1="#{path[0]['xo']}" x2="#{path[1]['xo']}" y1="#{path[0]['yo']}" y2="#{path[1]['yo']}"/>))
|
45
|
-
else
|
46
|
-
pts = path.map { |p| "#{p['xo']},#{p['yo']}" }
|
47
|
-
out.push(%(<polyline #{linestyle} points="#{pts.join(' ')}"/>))
|
48
|
-
end
|
49
|
-
end
|
50
50
|
out.join("\n")
|
51
51
|
%>
|
52
52
|
</svg>
|