diagrammatron 0.4.1 → 0.4.3
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 +74 -175
- data/bin/diagrammatron-nodes +1 -1
- data/bin/diagrammatron-render +54 -4
- data/bin/diagrammatron-template +19 -6
- data/template/internal.yaml +3 -1
- data/template/root.yaml +2 -0
- data/template/svg_1.1.erb +40 -37
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f4b7974989ef344d2eaaae0c54f82567b8ed9edd9c7fb5d2dfc3e405895efa7
|
4
|
+
data.tar.gz: efdd52da27d648f9da7fed309ba668eae6237eeddd51a703c47c46c45c394435
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e235a20fb0fb4013e4f0006731743830f84500519d2ab4262b04caa9bae0962bd7da5127cd30e44c9e6bf891b735a02f10fe193149047fa6b54ec0bd6185f2e5
|
7
|
+
data.tar.gz: e6c4c3db889c83d23390ee874379215747fb4336fbb3f1f2f543226ab72788fe948a6394e500ea89ed4d4a013126a324b10fa5b49bf73be9233cd94e9781a024
|
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'
|
@@ -158,6 +158,10 @@ Segment = Struct.new(:vertical, :cc, :range, :edge_index, :at_node, :segment_ind
|
|
158
158
|
def increase?
|
159
159
|
range[0] < range[1]
|
160
160
|
end
|
161
|
+
|
162
|
+
def within(s)
|
163
|
+
s.range.min < range.min && range.max < s.range.max
|
164
|
+
end
|
161
165
|
end
|
162
166
|
|
163
167
|
def segment(x0, y0, x1, y1)
|
@@ -415,126 +419,16 @@ def path_order_at_side(a, b, conn)
|
|
415
419
|
d
|
416
420
|
end
|
417
421
|
|
418
|
-
def
|
419
|
-
d = a[
|
422
|
+
def length_order(a, b)
|
423
|
+
d = a[2].length <=> b[2].length
|
420
424
|
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 # Top left, vertical, bottom right: -|_
|
428
|
-
d = a[2].range.min <=> b[2].range.min # Ascend on minimum.
|
429
|
-
return d unless d.zero?
|
430
|
-
d = a[2].length <=> b[2].length # Ascend on length.
|
431
|
-
return d unless d.zero?
|
432
|
-
when 2 # From top right, down, bottom left: _|-
|
433
|
-
d = a[2].range.max <=> b[2].range.max # Descend on maximum.
|
434
|
-
return -d unless d.zero?
|
435
|
-
d = a[2].length <=> b[2].length # Ascend on length.
|
436
|
-
return d unless d.zero?
|
437
|
-
when 3 # Descending on length, range maximum. |=
|
438
|
-
d = a[2].length <=> b[2].length
|
439
|
-
return -d unless d.zero?
|
440
|
-
d = a[2].range.max <=> b[2].range.max
|
441
|
-
return -d unless d.zero?
|
442
|
-
end
|
443
425
|
a[0].edge_index <=> b[0].edge_index
|
444
426
|
end
|
445
427
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
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
|
-
def zigzag_order(from_right_up_to_left, from_left_up_to_right)
|
495
|
-
# Interleave the two sets.
|
496
|
-
out = []
|
497
|
-
from_right_up_to_left.each { |x| out.push x }
|
498
|
-
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
|
-
out
|
428
|
+
def min_order(a, b)
|
429
|
+
d = a[2].range.min <=> b[2].range.min
|
430
|
+
return d unless d.zero?
|
431
|
+
a[0].edge_index <=> b[0].edge_index
|
538
432
|
end
|
539
433
|
|
540
434
|
def overlaps_set(c, others)
|
@@ -544,47 +438,59 @@ def overlaps_set(c, others)
|
|
544
438
|
false
|
545
439
|
end
|
546
440
|
|
547
|
-
def
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
441
|
+
def layered_order(u_shaped, z_shaped)
|
442
|
+
u_shaped.sort! { |a, b| length_order(a, b) }
|
443
|
+
z_shaped.sort! { |a, b| min_order(a, b) }
|
444
|
+
out = []
|
445
|
+
layer = []
|
446
|
+
until u_shaped.empty?
|
447
|
+
rejects = []
|
448
|
+
found = false
|
449
|
+
u_shaped.each do |a|
|
450
|
+
unless overlaps_set(a[2], layer)
|
451
|
+
found = true
|
452
|
+
# Creates crossing by putting in front of shorter segment fully within?
|
453
|
+
rejects.each do |r|
|
454
|
+
if r[2].within(a[2])
|
455
|
+
found = false
|
456
|
+
break
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
if found
|
461
|
+
layer.push a
|
462
|
+
found = false
|
463
|
+
else
|
464
|
+
rejects.push a
|
465
|
+
end
|
466
|
+
end
|
467
|
+
u_shaped = rejects
|
468
|
+
unless layer.empty?
|
469
|
+
out.concat layer
|
470
|
+
out.push nil
|
471
|
+
layer = []
|
558
472
|
end
|
559
|
-
next if fit
|
560
|
-
cands.push(cands.last + 1)
|
561
|
-
off[cands.last] = [ a ]
|
562
473
|
end
|
563
|
-
|
564
|
-
|
565
|
-
|
474
|
+
until z_shaped.empty?
|
475
|
+
rejects = []
|
476
|
+
found = false
|
477
|
+
z_shaped.each do |a|
|
478
|
+
if overlaps_set(a[2], layer)
|
479
|
+
rejects.push a
|
480
|
+
else
|
481
|
+
layer.push a
|
482
|
+
found = true
|
483
|
+
end
|
484
|
+
end
|
485
|
+
z_shaped = rejects
|
486
|
+
unless layer.empty?
|
487
|
+
out.concat layer
|
488
|
+
out.push nil
|
489
|
+
layer = []
|
566
490
|
end
|
567
491
|
end
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
def group_stacked_offsets(group)
|
572
|
-
return 0 if group.empty?
|
573
|
-
group.each_index do |k|
|
574
|
-
group[k][0].offset = k + 1
|
575
|
-
end
|
576
|
-
return group.size
|
577
|
-
%q(
|
578
|
-
# This produces narrower gaps but they may be less clear.
|
579
|
-
prev = group[0]
|
580
|
-
prev[0].offset = 1
|
581
|
-
(1...group.size).each do |k|
|
582
|
-
g = group[k]
|
583
|
-
g[0].offset = prev[0].offset + (g[2].range_overlap(prev[2]) ? 1 : 0)
|
584
|
-
prev = g
|
585
|
-
end
|
586
|
-
prev[0].offset
|
587
|
-
)
|
492
|
+
out.concat(layer) unless layer.empty?
|
493
|
+
out
|
588
494
|
end
|
589
495
|
|
590
496
|
def direct_range(paths)
|
@@ -717,29 +623,22 @@ def place_edges(work)
|
|
717
623
|
end
|
718
624
|
gaps.each_value do |direction|
|
719
625
|
direction.each_value do |gap|
|
720
|
-
gap.sort! { |a, b| segment_order(a, b) }
|
721
626
|
gleft = gap.select { |a| a[1].zero? }
|
627
|
+
grul = gap.select { |a| a[1] == 1 }
|
628
|
+
glur = gap.select { |a| a[1] == 2 }
|
722
629
|
gright = gap.select { |a| a[1] == 3 }
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
denominator = 1 + c[0]
|
736
|
-
(1...c.size).each do |k|
|
737
|
-
denominator += c[k]
|
738
|
-
before[k] = c[k - 1] + before[k - 1]
|
739
|
-
end
|
740
|
-
gap.each do |sg|
|
741
|
-
sg[0].offset = c[sg[1]] + 1 - sg[0].offset if sg[1] > 1
|
742
|
-
sg[0].offset = Rational(sg[0].offset + before[sg[1]], denominator)
|
630
|
+
all = layered_order(gleft, grul)
|
631
|
+
all.push nil
|
632
|
+
all.concat(layered_order(gright, glur).reverse)
|
633
|
+
# Give each rational offset using layer index + 1 and layer count + 2.
|
634
|
+
denominator = 2 + all.count { |x| x.nil? }
|
635
|
+
layer = 1
|
636
|
+
all.each do |sg|
|
637
|
+
if sg.nil?
|
638
|
+
layer += 1
|
639
|
+
else
|
640
|
+
sg[0].offset = Rational(layer, denominator)
|
641
|
+
end
|
743
642
|
end
|
744
643
|
end
|
745
644
|
end
|
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,13 @@ 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', {}) # No default in styles.
|
84
|
+
else
|
85
|
+
m['default'] = m.fetch('default', {}).merge(d)
|
86
|
+
d = m['default']
|
87
|
+
end
|
83
88
|
styles.fetch(group, {}).each_pair do |name, values|
|
84
89
|
s = d.clone
|
85
90
|
s.merge!(values) unless name == 'default'
|
@@ -107,7 +112,7 @@ class Styles
|
|
107
112
|
end
|
108
113
|
s.merge!(mapping['default']) unless found # Merge default at least.
|
109
114
|
# Keep values specified explicitly.
|
110
|
-
item.merge!(s) {|key, existing, from_template| existing }
|
115
|
+
item.merge!(s) { |key, existing, from_template| existing || from_template }
|
111
116
|
end
|
112
117
|
|
113
118
|
def apply_node_styles(node)
|
@@ -276,6 +281,27 @@ def apply(doc, template)
|
|
276
281
|
out
|
277
282
|
end
|
278
283
|
|
284
|
+
def reverse_depth_order(a, b)
|
285
|
+
d = b['depth'] <=> a['depth']
|
286
|
+
return d unless d.zero?
|
287
|
+
d = a['index'] <=> b['index']
|
288
|
+
return d unless d.zero?
|
289
|
+
a['kind'] <=> b['kind']
|
290
|
+
end
|
291
|
+
|
292
|
+
def reverse_depth_sort(items)
|
293
|
+
arr = []
|
294
|
+
items.each_index do |k|
|
295
|
+
arr.push({
|
296
|
+
'depth' => items[k].fetch('depth', 0),
|
297
|
+
'index' => k,
|
298
|
+
'kind' => items[k].fetch('kind', 'unknown')
|
299
|
+
})
|
300
|
+
end
|
301
|
+
arr.sort! { |a, b| reverse_depth_order(a, b) }
|
302
|
+
arr.map { |x| items[x['index']] }
|
303
|
+
end
|
304
|
+
|
279
305
|
def main
|
280
306
|
template = nil
|
281
307
|
input = nil
|
@@ -350,6 +376,30 @@ Output is the file produced by the erb-template.
|
|
350
376
|
remap_coordinates(xcoords, xmax, x2min, doc.dig('diagram', 'edge_gap'))
|
351
377
|
remap_coordinates(ycoords, ymax, y2min, doc.dig('diagram', 'edge_gap'))
|
352
378
|
|
379
|
+
doc['nodes'] = reverse_depth_sort(doc.fetch('nodes', []))
|
380
|
+
doc['edges'] = reverse_depth_sort(doc.fetch('edges', []))
|
381
|
+
all = doc.fetch('nodes', []).map do |a|
|
382
|
+
{
|
383
|
+
'kind' => 'node',
|
384
|
+
'depth' => a.fetch('depth', 0),
|
385
|
+
'item' => a
|
386
|
+
}
|
387
|
+
end
|
388
|
+
all.concat(doc.fetch('edges', []).map do |a|
|
389
|
+
{
|
390
|
+
'kind' => 'edge',
|
391
|
+
'depth' => a.fetch('depth', 0),
|
392
|
+
'item' => a
|
393
|
+
}
|
394
|
+
end)
|
395
|
+
all = reverse_depth_sort(all)
|
396
|
+
doc['all'] = all.map do |x|
|
397
|
+
{
|
398
|
+
'kind' => x['kind'],
|
399
|
+
'item' => x['item']
|
400
|
+
}
|
401
|
+
end
|
402
|
+
|
353
403
|
dump_result(output, apply(doc, template), 5)
|
354
404
|
end
|
355
405
|
|
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
@@ -19,6 +19,7 @@ styles:
|
|
19
19
|
fill: "#ffffff"
|
20
20
|
stroke: "#000000"
|
21
21
|
stroke_width: 2
|
22
|
+
depth: 0
|
22
23
|
size_estimator: |
|
23
24
|
$render.default_size($render.node['font_size'],
|
24
25
|
$render.node['font_width'], $render.node['font_height'],
|
@@ -29,4 +30,5 @@ styles:
|
|
29
30
|
default:
|
30
31
|
stroke_width: 2
|
31
32
|
stroke: "#000000"
|
32
|
-
|
33
|
+
depth: 0
|
34
|
+
base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpoaCArPSAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnaGVpZ2h0X21hcmdpbicpCgpvdXQgPSBbCiAgJSg8c3ZnIHdpZHRoPSIje3cgKyAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnd2lkdGhfbWFyZ2luJyl9IiBoZWlnaHQ9IiN7aGh9IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiPikKXQokcmVuZGVyLmRvY1snYWxsJ10uZWFjaCBkbyB8aXRlbXwKICBpZiBpdGVtWydraW5kJ10gPT0gJ25vZGUnCiAgICBub2RlID0gaXRlbVsnaXRlbSddCiAgICB3ID0gbm9kZVsndyddLnRvX2kKICAgIGggPSBub2RlWydoJ10udG9faQogICAgeCA9IG5vZGVbJ3hvJ10udG9faQogICAgeSA9IGhoIC0gbm9kZVsneW8nXS50b19pIC0gaAogICAgbm9kZXN0eWxlID0gJShmaWxsPSIje25vZGVbJ2ZpbGwnXX0iIHN0cm9rZT0iI3tub2RlWydzdHJva2UnXX0iIHN0cm9rZS13aWR0aD0iI3tub2RlWydzdHJva2Vfd2lkdGgnXX0iKQogICAgb3V0LnB1c2goJSg8cmVjdCAje25vZGVzdHlsZX0gaGVpZ2h0PSIje2h9IiB3aWR0aD0iI3t3fSIgeD0iI3t4fSIgeT0iI3t5fSIvPikpCiAgICB4ICs9IG5vZGVbJ3dpZHRoX21hcmdpbiddCiAgICBmcyA9IG5vZGVbJ2ZvbnRfc2l6ZSddCiAgICBsaCA9IGZzICogKDEgKyBub2RlWydmb250X2xpbmVfc3BhY2luZyddKQogICAgeSArPSBub2RlWydoZWlnaHRfbWFyZ2luJ10gKyBmcyAqIG5vZGVbJ2ZvbnRfYXNjZW5kJ10gIyBCYXNlbGluZSBmb3IgZmlyc3QgbGluZS4KICAgIHVybCA9IG5vZGUuZmV0Y2goJ3VybCcsIG5pbCkKICAgIHVybC5lbmNvZGUhKDp4bWwgPT4gOmF0dHIpIHVubGVzcyB1cmwubmlsPwogICAgeTAgPSB5CiAgICB0ZXh0c3R5bGUgPSAlKGZpbGw9IiN7bm9kZVsnZm9udF9maWxsJ119IiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iI3tub2RlWydmb250X2ZpbGwnXX0iIHN0cm9rZS13aWR0aD0iMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIpCiAgICBsaW5rc3R5bGUgPSAlKGZpbGw9IiN7bm9kZVsndXJsX2ZpbGwnXX0iIGZvbnQtZmFtaWx5PSJzZXJpZiIgZm9udC1zaXplPSIje2ZzfSIgc3Ryb2tlPSIje25vZGVbJ3VybF9maWxsJ119IiBzdHJva2Utd2lkdGg9IjAiIHhtbDpzcGFjZT0icHJlc2VydmUiKQogICAgbm9kZVsndGV4dCddLmVhY2ggZG8gfGxpbmV8CiAgICAgIGxpbmUuZW5jb2RlISg6eG1sID0+IDp0ZXh0KQogICAgICBpZiB1cmwubmlsPwogICAgICAgIG91dC5wdXNoKCUoPHRleHQgI3t0ZXh0c3R5bGV9IHg9IiN7eH0iIHk9IiN7eTB9Ij4je2xpbmV9PC90ZXh0PikpCiAgICAgIGVsc2UKICAgICAgICBvdXQucHVzaCglKDxhIHhsaW5rOmhyZWY9I3t1cmx9IHRhcmdldD0iX3BhcmVudCI+PHRleHQgI3tsaW5rc3R5bGV9IHg9IiN7eH0iIHk9IiN7eTB9Ij4je2xpbmV9PC90ZXh0PjwvYT4pKQogICAgICBlbmQKICAgICAgeTAgKz0gbGggIyBTaGlmdCBiYXNlbGluZSBieSBmdWxsIGxpbmUgKyBzcGFjaW5nIGhlaWdodC4KICAgIGVuZAogIGVsc2UKICAgIGVkZ2UgPSBpdGVtWydpdGVtJ10KICAgIGxpbmVzdHlsZSA9ICUoZmlsbD0ibm9uZSIgc3Ryb2tlPSIje2VkZ2VbJ3N0cm9rZSddfSIgc3Ryb2tlLXdpZHRoPSIje2VkZ2VbJ3N0cm9rZV93aWR0aCddfSIpCiAgICBwYXRoID0gZWRnZS5mZXRjaCgncGF0aCcsIG5pbCkKICAgIG5leHQgaWYgcGF0aC5uaWw/CiAgICBwYXRoLmVhY2ggZG8gfHB8CiAgICAgIHBbJ3hvJ10gPSBwWyd4byddLnRvX2kudG9fcwogICAgICBwWyd5byddID0gKGhoIC0gcFsneW8nXSkudG9faS50b19zCiAgICBlbmQKICAgIGlmIHBhdGguc2l6ZSA9PSAyCiAgICAgIG91dC5wdXNoKCUoPGxpbmUgI3tsaW5lc3R5bGV9IHgxPSIje3BhdGhbMF1bJ3hvJ119IiB4Mj0iI3twYXRoWzFdWyd4byddfSIgeTE9IiN7cGF0aFswXVsneW8nXX0iIHkyPSIje3BhdGhbMV1bJ3lvJ119Ii8+KSkKICAgIGVsc2UKICAgICAgcHRzID0gcGF0aC5tYXAgeyB8cHwgIiN7cFsneG8nXX0sI3twWyd5byddfSIgfQogICAgICBvdXQucHVzaCglKDxwb2x5bGluZSAje2xpbmVzdHlsZX0gcG9pbnRzPSIje3B0cy5qb2luKCcgJyl9Ii8+KSkKICAgIGVuZAogIGVuZAplbmQKb3V0LmpvaW4oIlxuIikKJT4KPC9zdmc+Cg==
|
data/template/root.yaml
CHANGED
@@ -18,6 +18,7 @@ styles:
|
|
18
18
|
fill: "#ffffff"
|
19
19
|
stroke: "#000000"
|
20
20
|
stroke_width: 2
|
21
|
+
depth: 0
|
21
22
|
size_estimator: |
|
22
23
|
$render.default_size($render.node['font_size'],
|
23
24
|
$render.node['font_width'], $render.node['font_height'],
|
@@ -28,3 +29,4 @@ styles:
|
|
28
29
|
default:
|
29
30
|
stroke_width: 2
|
30
31
|
stroke: "#000000"
|
32
|
+
depth: 0
|
data/template/svg_1.1.erb
CHANGED
@@ -6,45 +6,48 @@ 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
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
9
|
+
$render.doc['all'].each do |item|
|
10
|
+
if item['kind'] == 'node'
|
11
|
+
node = item['item']
|
12
|
+
w = node['w'].to_i
|
13
|
+
h = node['h'].to_i
|
14
|
+
x = node['xo'].to_i
|
15
|
+
y = hh - node['yo'].to_i - h
|
16
|
+
nodestyle = %(fill="#{node['fill']}" stroke="#{node['stroke']}" stroke-width="#{node['stroke_width']}")
|
17
|
+
out.push(%(<rect #{nodestyle} height="#{h}" width="#{w}" x="#{x}" y="#{y}"/>))
|
18
|
+
x += node['width_margin']
|
19
|
+
fs = node['font_size']
|
20
|
+
lh = fs * (1 + node['font_line_spacing'])
|
21
|
+
y += node['height_margin'] + fs * node['font_ascend'] # Baseline for first line.
|
22
|
+
url = node.fetch('url', nil)
|
23
|
+
url.encode!(:xml => :attr) unless url.nil?
|
24
|
+
y0 = y
|
25
|
+
textstyle = %(fill="#{node['font_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['font_fill']}" stroke-width="0" xml:space="preserve")
|
26
|
+
linkstyle = %(fill="#{node['url_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['url_fill']}" stroke-width="0" xml:space="preserve")
|
27
|
+
node['text'].each do |line|
|
28
|
+
line.encode!(:xml => :text)
|
29
|
+
if url.nil?
|
30
|
+
out.push(%(<text #{textstyle} x="#{x}" y="#{y0}">#{line}</text>))
|
31
|
+
else
|
32
|
+
out.push(%(<a xlink:href=#{url} target="_parent"><text #{linkstyle} x="#{x}" y="#{y0}">#{line}</text></a>))
|
33
|
+
end
|
34
|
+
y0 += lh # Shift baseline by full line + spacing height.
|
31
35
|
end
|
32
|
-
y0 += lh # Shift baseline by full line + spacing height.
|
33
|
-
end
|
34
|
-
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
36
|
else
|
46
|
-
|
47
|
-
|
37
|
+
edge = item['item']
|
38
|
+
linestyle = %(fill="none" stroke="#{edge['stroke']}" stroke-width="#{edge['stroke_width']}")
|
39
|
+
path = edge.fetch('path', nil)
|
40
|
+
next if path.nil?
|
41
|
+
path.each do |p|
|
42
|
+
p['xo'] = p['xo'].to_i.to_s
|
43
|
+
p['yo'] = (hh - p['yo']).to_i.to_s
|
44
|
+
end
|
45
|
+
if path.size == 2
|
46
|
+
out.push(%(<line #{linestyle} x1="#{path[0]['xo']}" x2="#{path[1]['xo']}" y1="#{path[0]['yo']}" y2="#{path[1]['yo']}"/>))
|
47
|
+
else
|
48
|
+
pts = path.map { |p| "#{p['xo']},#{p['yo']}" }
|
49
|
+
out.push(%(<polyline #{linestyle} points="#{pts.join(' ')}"/>))
|
50
|
+
end
|
48
51
|
end
|
49
52
|
end
|
50
53
|
out.join("\n")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: diagrammatron
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismo Kärkkäinen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
|