diagrammatron 0.4.1 → 0.4.2
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 +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>
|