diagrammatron 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d5e755dfc41bacfbfb226e77365b8d54a5ff0f01ea5b642febdbb1699497d21
4
- data.tar.gz: c9b8b67509f94635cd65b77ed89a3cf0a456f6c666a7b3773539760a8e429fec
3
+ metadata.gz: f620125fb02691c986e3f340cc3248fbab02ecc99cd075ed9b0a9dfd2159a541
4
+ data.tar.gz: b384432f4b082379eedce471810cd8ad011bc1e016dc5410c00b99e3b1187b70
5
5
  SHA512:
6
- metadata.gz: '081df6de2fb3489cc0976a2805c829fb846ce5561617b15d7446dda4a6bfd0a63bbc9abf234e89e895ed90f710f756cbc8b1f47618ee4216e89a2c4e522e9046'
7
- data.tar.gz: 85e77e7adf696cd3f46fe2b41a77266d67cf1d7e904cb1c9eb4f83bae6e3fef8db7f199313923e588c8802e8edb6d8148869f2c8ef93685aeb3af48dc6a83252
6
+ metadata.gz: edd885e247f16df32a67c6524cbfabfddaf9c6901fce6104c373fb6c7bd107c734fb275be133c09f06f128bade0bd34e9c626f87e14e097b580817a970277020
7
+ data.tar.gz: 3b7febb11202fc30fd0ba522d276e6f41ab4c25d72b2bf468c11ce8bd4b7f3c27001e7520e3e68705518e4b5646bb6708152ab766059cbd6b646f10b703e272c
@@ -68,7 +68,7 @@ def work_copy(src, quiet)
68
68
  next
69
69
  end
70
70
  label = node['label']
71
- if label2idx.key?(label) && edge_nodes.key?(label)
71
+ if label2idx.key?(label) && edge_nodes.member?(label)
72
72
  aargh "Edge-referred label used twice: #{label}"
73
73
  errors = true
74
74
  end
@@ -424,63 +424,111 @@ def segment_order(a, b)
424
424
  return d unless d.zero?
425
425
  d = a[2].range.min <=> b[2].range.min
426
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.
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.
429
431
  return d unless d.zero?
430
432
  when 2 # From top right, down, bottom left: _|-
431
- d = a[2].range.min <=> b[2].range.min # Guesstimate for DFS.
433
+ d = a[2].range.max <=> b[2].range.max # Descend on maximum.
432
434
  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
435
+ d = a[2].length <=> b[2].length # Ascend on length.
437
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?
438
442
  end
439
443
  a[0].edge_index <=> b[0].edge_index
440
444
  end
441
445
 
442
- GapState = Struct.new(:order, :cross_count) do
446
+ GapState = Struct.new(:order, :cross_count, :lut) do
443
447
  def fitness(segments, k)
448
+ table = lut[k]
444
449
  crossings = 0
445
- s = segments[k]
446
- prev = (s[1] == 1) ? s[2].range.max : s[2].range.min
447
450
  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
451
+ crossings += table[n]
456
452
  end
457
453
  cross_count + crossings
458
454
  end
459
455
  end
460
456
 
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.cross_count) <= c
467
- state.order.push(k)
468
- best = depth_first_search(segments, GapState.new(state.order, c), best)
469
- state.order.pop
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
470
471
  end
471
- else
472
- best = GapState.new(state.order.clone, state.cross_count)
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
473
490
  end
474
491
  best
475
492
  end
476
493
 
477
- def zigzag_order(segments)
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
478
500
  return segments if segments.size < 2
479
501
  # DFS. Fitness is how many end segments cross the placed segments.
480
502
  # Sort so that those that cross least when at start are first and vice versa.
481
- best = nil
503
+ lut = {}
482
504
  segments.each_index do |k|
483
- best = depth_first_search(segments, GapState.new([ k ], 0), best)
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)
484
532
  end
485
533
  out = []
486
534
  best.order.each do |k|
@@ -653,7 +701,7 @@ def place_edges(work)
653
701
  chosen.each_value do |p|
654
702
  (1...(p.segments.size - 1)).each do |k|
655
703
  # Middle segments always have surrounding segments.
656
- s, before, after, sb, sa = p.segment_directions(k)
704
+ s, below, above, sb, sa = p.segment_directions(k)
657
705
  so = s.clone # More accurate info on actual range with end offsets.
658
706
  if so.range[0] < so.range[1]
659
707
  so.range[0] += sb.offset unless sb.offset.nil?
@@ -662,7 +710,7 @@ def place_edges(work)
662
710
  so.range[0] += sb.offset.nil? ? 0.9999 : sb.offset
663
711
  so.range[1] += sa.offset unless sa.offset.nil?
664
712
  end
665
- group = (before.negative? ? 0 : 1) + (after.negative? ? 0 : 2)
713
+ group = (below.negative? ? 0 : 1) + (above.negative? ? 0 : 2)
666
714
  d = gaps[s.vertical]
667
715
  d[s.cc] = d.fetch(s.cc, []).push([ s, group, so ])
668
716
  end
@@ -672,7 +720,7 @@ def place_edges(work)
672
720
  gap.sort! { |a, b| segment_order(a, b) }
673
721
  gleft = gap.select { |a| a[1].zero? }
674
722
  gright = gap.select { |a| a[1] == 3 }
675
- gmiddle = zigzag_order(gap.select { |a| a[1] == 1 || a[1] == 2 })
723
+ gmiddle = zigzag_order(gap.select() { |a| a[1] == 1 }, gap.select() { |a| a[1] == 2 })
676
724
  gmiddle.each do |s|
677
725
  s[1] = 1
678
726
  end
@@ -312,7 +312,7 @@ def work_copy(src, quiet)
312
312
  next
313
313
  end
314
314
  label = node['label']
315
- if label2idx.key?(label) && edge_nodes.key?(label)
315
+ if label2idx.key?(label) && edge_nodes.member?(label)
316
316
  aargh "Edge-referred label used twice: #{label}"
317
317
  errors = true
318
318
  end
@@ -76,94 +76,101 @@ def separate_coordinates(doc)
76
76
  [ xcoords, ycoords, ckd2count ]
77
77
  end
78
78
 
79
- class FontInfo
80
- attr_reader :descender, :max_ascend, :ascender, :cap_height, :line_spacing, :width, :size
81
-
82
- def initialize(template)
83
- font = template.fetch('defaults', {}).fetch('font', { 'font_size' => 16 })
84
- @cap_height = font.fetch('cap_height', 0.8 * font.fetch('font_size', 16))
85
- @ascender = font.fetch('ascender', @cap_height)
86
- @max_ascend = [ @cap_height, @ascender ].max
87
- @descender = font.fetch('descender', 0.25 * @max_ascend)
88
- @size = font.fetch('font_size', @max_ascend + @descender)
89
- @line_spacing = font.fetch('line_spacing', 0.2 * @max_ascend)
90
- @width = font.fetch('width', 0.5 * @size)
79
+ class Styles
80
+ def base_styles(m, styles, group)
81
+ d = styles.dig(group, 'default') || {}
82
+ m['default'] = m.fetch('default', {}).merge(d)
83
+ styles.fetch(group, {}).each_pair do |name, values|
84
+ s = d.clone
85
+ s.merge!(values) unless name == 'default'
86
+ m[name] = m.fetch(name, {}).merge(s)
87
+ end
88
+ m
89
+ end
90
+
91
+ def initialize(template_styles, diagram_styles)
92
+ @n = base_styles(base_styles({}, template_styles, 'node'), diagram_styles, 'node')
93
+ @e = base_styles(base_styles({}, template_styles, 'edge'), diagram_styles, 'edge')
94
+ @d = base_styles(base_styles({}, template_styles, 'diagram'), diagram_styles, 'diagram')
95
+ end
96
+
97
+ def fill(mapping, type_name, item)
98
+ styles = item.fetch('style', [ 'default' ])
99
+ styles = [ styles ] unless styles.is_a?(Array)
100
+ s = {}
101
+ found = false
102
+ styles.each do |name|
103
+ ns = mapping.fetch(name, nil)
104
+ next if ns.nil?
105
+ found = true
106
+ s.merge! ns
107
+ end
108
+ s.merge!(mapping['default']) unless found # Merge default at least.
109
+ # Keep values specified explicitly.
110
+ item.merge!(s) {|key, existing, from_template| existing }
111
+ end
112
+
113
+ def apply_node_styles(node)
114
+ fill(@n, 'node', node)
91
115
  end
92
- end
93
116
 
94
- class Defaults
95
- attr_reader :width_key, :height_key, :width_margin, :height_margin, :edge_gap
96
- attr_reader :font
97
-
98
- def initialize(template)
99
- defaults = template.fetch('defaults', {})
100
- @width_key = defaults.fetch('width_key', 'w')
101
- @height_key = defaults.fetch('height_key', 'h')
102
- @width_margin = defaults.fetch('width_margin', 10)
103
- @height_margin = defaults.fetch('height_margin', 10)
104
- @edge_gap = defaults.fetch('edge_gap', 20)
105
- @font = FontInfo.new(template)
117
+ def apply_edge_styles(edge)
118
+ fill(@e, 'edge', edge)
119
+ end
120
+
121
+ def apply_diagram_styles(diagram)
122
+ fill(@d, 'diagram', diagram)
106
123
  end
107
124
  end
108
125
 
109
126
  class SizeEstimation
110
- attr_accessor :node, :template, :ckd2count, :defaults
127
+ attr_accessor :node, :ckd2count, :doc
111
128
 
112
- def initialize(template, ckd2count, defaults)
129
+ def initialize(ckd2count, doc)
113
130
  @node = nil
114
- @template = template
115
131
  @ckd2count = ckd2count
116
- @defaults = defaults
132
+ @doc = doc
117
133
  end
118
134
 
119
- def get_binding
135
+ def exposed_binding
120
136
  binding
121
137
  end
122
138
 
123
- def get_default(key, default_value = nil)
124
- @template.fetch('defaults', {}).fetch(key, default_value)
125
- end
126
-
127
139
  def max_edges(key, edge_gap)
128
140
  c = [ @node['xo'], @node['yo'], key, -1 ]
129
141
  count = @ckd2count[c]
130
142
  c[3] = 1
131
143
  count = [ count, @ckd2count[c] ].max
132
144
  return 0 if count < 2
133
- (count - 1) * (edge_gap || @defaults.edge_gap)
145
+ (count - 1) * edge_gap
134
146
  end
135
147
 
136
- def default_size(width_scale = nil, height_scale = nil, line_spacing = nil,
137
- width_margin = nil, height_margin = nil, edge_gap = nil)
138
- lines = @node['label']
139
- w = 2 * (width_margin || @defaults.width_margin) +
140
- (width_scale || @default.font.width) * (lines.map &(:size)).max
141
- @node[@defaults.width_key] = [ w, max_edges('xo', edge_gap) ].max
142
- h = 2 * (height_margin || @defaults.height_margin) +
143
- (height_scale || @defaults.font.size) * lines.size +
144
- (line_spacing || @defaults.font.line_spacing) * (lines.size - 1)
145
- @node[@defaults.height_key] = [ h, max_edges('yo', edge_gap) ].max
148
+ # font_size is the actual size.
149
+ # font_width, font_height, font_line_spacing are [0, 1] size scaling factors.
150
+ # width_margin, height_margin are in same units as fonti, space inside node.
151
+ # edge_gap is minimum space between edges at any node side.
152
+ def default_size(font_size, font_width, font_height, font_line_spacing,
153
+ width_margin, height_margin, edge_gap)
154
+ lines = @node['text']
155
+ w = 2 * width_margin + font_width * font_size * (lines.map &(:size)).max
156
+ @node['w'] = [ w, max_edges('xo', edge_gap) ].max
157
+ h = 2 * height_margin + font_height * font_size * lines.size +
158
+ font_line_spacing * font_size * (lines.size - 1)
159
+ @node['h'] = [ h, max_edges('yo', edge_gap) ].max
146
160
  end
147
161
  end
148
162
 
149
- def estimate_sizes(doc, template, ckd2count, defaults)
150
- $render = SizeEstimation.new(template, ckd2count, defaults)
151
- sizes = template.fetch('sizes', {})
152
- defaults = template.fetch('defaults', {})
163
+ def estimate_sizes(doc, ckd2count)
164
+ $render = SizeEstimation.new(ckd2count, doc)
153
165
  doc['nodes'].each do |node|
154
- label = node.fetch('label', 'unnamed')
155
- # Substitute "text" as label if present, split to lines.
156
- node['label'] = node.fetch('text', node.fetch('label', '')).split("\n")
157
166
  $render.node = node
167
+ label = node.fetch('label', 'unnamed')
158
168
  style = node.fetch('style', 'default')
159
- code = sizes.fetch(style, defaults.fetch('size',
160
- %(raise NotImplementedError, "No size estimator for style: #{style}")))
161
- if sizes.key? code
162
- code = sizes.fetch(code)
163
- end
169
+ code = node.fetch('size_estimator',
170
+ %(raise NotImplementedError, "No size estimator for style: #{style}"))
164
171
  code = code.join("\n") if code.is_a? Array
165
172
  begin
166
- eval(code, $render.get_binding)
173
+ eval(code, $render.exposed_binding)
167
174
  rescue StandardError => e
168
175
  return aargh("Size estimate style #{style} node #{label} error #{e}", false)
169
176
  end
@@ -172,20 +179,20 @@ def estimate_sizes(doc, template, ckd2count, defaults)
172
179
  true
173
180
  end
174
181
 
175
- def maxima(doc, defaults)
182
+ def maxima(doc)
176
183
  xmax = Hash.new(0)
177
184
  ymax = Hash.new(0)
178
185
  doc.fetch('nodes', []).each do |node|
179
- xmax[node['xo']] = [ node[defaults.width_key], xmax[node['xo']] ].max
180
- ymax[node['yo']] = [ node[defaults.height_key], ymax[node['yo']] ].max
186
+ xmax[node['xo']] = [ node['w'], xmax[node['xo']] ].max
187
+ ymax[node['yo']] = [ node['h'], ymax[node['yo']] ].max
181
188
  end
182
189
  [ xmax, ymax ]
183
190
  end
184
191
 
185
- def apply_maxima(doc, xmax, ymax, defaults)
192
+ def apply_maxima(doc, xmax, ymax)
186
193
  doc.fetch('nodes', []).each do |node|
187
- node[defaults.width_key] = xmax[node['xo']]
188
- node[defaults.height_key] = ymax[node['yo']]
194
+ node['w'] = xmax[node['xo']]
195
+ node['h'] = ymax[node['yo']]
189
196
  end
190
197
  end
191
198
 
@@ -200,8 +207,8 @@ def parallel_edge_step_minima(coords)
200
207
  c2m
201
208
  end
202
209
 
203
- def remap_coordinates(coords, cmax, c2min, defaults)
204
- c = defaults.edge_gap
210
+ def remap_coordinates(coords, cmax, c2min, edge_gap)
211
+ c = edge_gap
205
212
  gap = 0 # How much space all edge segments need.
206
213
  zero_after_decrease = false
207
214
  prev_dir = -2
@@ -210,19 +217,19 @@ def remap_coordinates(coords, cmax, c2min, defaults)
210
217
  case coord.direction
211
218
  when -1
212
219
  c += gap if -1 < prev_dir
213
- gap = defaults.edge_gap
220
+ gap = edge_gap
214
221
  coord.object[coord.key] = c
215
222
  when 0
216
- gap = defaults.edge_gap / c2min[coord.integer]
223
+ gap = edge_gap / c2min[coord.integer]
217
224
  if zero_after_decrease
218
225
  # Edge segment is at same range as nodes.
219
226
  coord.object[coord.key] = c + coord.fraction * cmax[coord.integer]
220
227
  else
221
228
  coord.object[coord.key] =
222
- c + (defaults.edge_gap * coord.fraction) / c2min[coord.integer]
229
+ c + (edge_gap * coord.fraction) / c2min[coord.integer]
223
230
  end
224
231
  when 1
225
- gap = defaults.edge_gap
232
+ gap = edge_gap
226
233
  c += cmax[coord.integer] unless prev_dir == 1
227
234
  coord.object[coord.key] = c
228
235
  zero_after_decrease = false
@@ -232,28 +239,23 @@ def remap_coordinates(coords, cmax, c2min, defaults)
232
239
  end
233
240
 
234
241
  class Render
235
- attr_accessor :doc, :template, :defaults
242
+ attr_accessor :doc, :template
236
243
 
237
- def initialize(doc, template, defaults)
244
+ def initialize(doc, template)
238
245
  @doc = doc
239
246
  @template = template
240
- @defaults = defaults
241
247
  end
242
248
 
243
- def get_binding
249
+ def exposed_binding
244
250
  binding
245
251
  end
246
252
 
247
- def get_default(key, default_value = nil)
248
- @template.fetch('defaults', {}).fetch(key, default_value)
249
- end
250
-
251
253
  def dimensions
252
254
  w = 0
253
255
  h = 0
254
256
  @doc.fetch('nodes', []).each do |node|
255
- w = [ w, node['xo'] + node[@defaults.width_key] ].max
256
- h = [ h, node['yo'] + node[@defaults.height_key] ].max
257
+ w = [ w, node['xo'] + node['w'] ].max
258
+ h = [ h, node['yo'] + node['h'] ].max
257
259
  end
258
260
  @doc.fetch('edges', []).each do |edge|
259
261
  path = edge.fetch('path', nil)
@@ -267,9 +269,9 @@ class Render
267
269
  end
268
270
  end
269
271
 
270
- def apply(doc, template, defaults)
271
- $render = Render.new(doc, template, defaults)
272
- out = ERB.new(template.fetch('template', '')).result($render.get_binding)
272
+ def apply(doc, template)
273
+ $render = Render.new(doc, template)
274
+ out = ERB.new(template.fetch('template', '')).result($render.exposed_binding)
273
275
  $render = nil
274
276
  out
275
277
  end
@@ -278,6 +280,7 @@ def main
278
280
  template = nil
279
281
  input = nil
280
282
  output = nil
283
+ styles = nil
281
284
  parser = OptionParser.new do |opts|
282
285
  opts.summary_indent = ' '
283
286
  opts.summary_width = 20
@@ -317,29 +320,37 @@ Output is the file produced by the erb-template.
317
320
  return aargh("Key #{key} base-64 decoding failed to key #{nk}", 2)
318
321
  end
319
322
  end
320
- defaults = Defaults.new(template)
321
323
 
322
324
  doc = load_source(input)
323
325
  return 2 if doc.nil?
324
326
 
327
+ styles = Styles.new(template.fetch('styles', {}), doc.fetch('styles', {}))
328
+ doc.fetch('nodes', []).each do |node|
329
+ styles.apply_node_styles(node)
330
+ node['text'] = node.fetch('text', node.fetch('label', '')).split("\n")
331
+ end
332
+ doc.fetch('edges', []).each { |edge| styles.apply_edge_styles(edge) }
333
+ doc['diagram'] = {} unless doc.key? 'diagram'
334
+ styles.apply_diagram_styles(doc['diagram'])
335
+
325
336
  begin
326
337
  xcoords, ycoords, ckd2count = separate_coordinates(doc)
327
338
  rescue StandardError
328
339
  return aargh('Error processing input.', 3)
329
340
  end
330
341
 
331
- return 4 unless estimate_sizes(doc, template, ckd2count, defaults)
342
+ return 4 unless estimate_sizes(doc, ckd2count)
332
343
 
333
344
  # Make all rows the same height and all columns the same width.
334
- xmax, ymax = maxima(doc, defaults)
335
- apply_maxima(doc, xmax, ymax, defaults)
345
+ xmax, ymax = maxima(doc)
346
+ apply_maxima(doc, xmax, ymax)
336
347
 
337
348
  x2min = parallel_edge_step_minima(xcoords)
338
349
  y2min = parallel_edge_step_minima(ycoords)
339
- remap_coordinates(xcoords, xmax, x2min, defaults)
340
- remap_coordinates(ycoords, ymax, y2min, defaults)
350
+ remap_coordinates(xcoords, xmax, x2min, doc.dig('diagram', 'edge_gap'))
351
+ remap_coordinates(ycoords, ymax, y2min, doc.dig('diagram', 'edge_gap'))
341
352
 
342
- dump_result(output, apply(doc, template, defaults), 5)
353
+ dump_result(output, apply(doc, template), 5)
343
354
  end
344
355
 
345
356
  exit(main) if (defined? $unit_test).nil?
@@ -21,7 +21,7 @@ def add_field(doc, field_name, content)
21
21
  end
22
22
 
23
23
  def missing(doc)
24
- %w[defaults sizes template].each do |key|
24
+ %w[template].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)
@@ -1,21 +1,32 @@
1
1
  ---
2
- defaults:
3
- edge_gap: 20
4
- width_margin: 10
5
- height_margin: 10
6
- font:
7
- font_size: 16
8
- stroke_width: 2
9
- width_key: w
10
- height_key: h
11
- size: default
12
- sizes:
13
- default: |
14
- $render.default_size(
15
- $render.node.fetch('width_scale', $render.defaults.font.width),
16
- $render.node.fetch('height_scale', $render.defaults.font.size),
17
- $render.node.fetch('line_spacing', $render.defaults.font.line_spacing),
18
- $render.node.fetch('width_margin', $render.defaults.width_margin),
19
- $render.node.fetch('height_margin', $render.defaults.height_margin),
20
- $render.node.fetch('edge_gap', $render.defaults.edge_gap))
21
- base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpobSA9ICRyZW5kZXIuZGVmYXVsdHMuaGVpZ2h0X21hcmdpbgpoaCArPSBobQptYSA9ICRyZW5kZXIuZGVmYXVsdHMuZm9udC5tYXhfYXNjZW5kCmZzID0gJHJlbmRlci5kZWZhdWx0cy5mb250LnNpemUKbGggPSBmcyArICRyZW5kZXIuZGVmYXVsdHMuZm9udC5saW5lX3NwYWNpbmcKd20gPSAkcmVuZGVyLmRlZmF1bHRzLndpZHRoX21hcmdpbgpzdyA9ICRyZW5kZXIuZ2V0X2RlZmF1bHQoJ3N0cm9rZV93aWR0aCcsIDUpCmxpbmVzdHlsZSA9ICUoZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IiN7c3d9IikKdGV4dHN0eWxlID0gJShmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIwIiB4bWw6c3BhY2U9InByZXNlcnZlIikKbGlua3N0eWxlID0gJShmaWxsPSIjMjAyMGZmIiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iIzIwMjBmZiIgc3Ryb2tlLXdpZHRoPSIwIiB4bWw6c3BhY2U9InByZXNlcnZlIikKCm91dCA9IFsKICAlKDxzdmcgd2lkdGg9IiN7dyArICRyZW5kZXIuZGVmYXVsdHMud2lkdGhfbWFyZ2lufSIgaGVpZ2h0PSIje2hofSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIj4pCl0KJHJlbmRlci5kb2MuZmV0Y2goJ25vZGVzJywgW10pLmVhY2ggZG8gfG5vZGV8CiAgdyA9IG5vZGVbJHJlbmRlci5kZWZhdWx0cy53aWR0aF9rZXldLnRvX2kKICBoID0gbm9kZVskcmVuZGVyLmRlZmF1bHRzLmhlaWdodF9rZXldLnRvX2kKICB4ID0gbm9kZVsneG8nXS50b19pCiAgeSA9IGhoIC0gbm9kZVsneW8nXS50b19pIC0gaAogIG5vZGVzdHlsZSA9ICUoZmlsbD0iI3tub2RlLmZldGNoKCdmaWxsJywgJyNmZmZmZmYnKX0iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIje3N3fSIpCiAgb3V0LnB1c2goJSg8cmVjdCAje25vZGVzdHlsZX0gaGVpZ2h0PSIje2h9IiB3aWR0aD0iI3t3fSIgeD0iI3t4fSIgeT0iI3t5fSIvPikpCiAgeCArPSB3bQogIHkgKz0gaG0gKyBtYSAjIEJhc2VsaW5lIGZvciBmaXJzdCBsaW5lLgogIHVybCA9IG5vZGUuZmV0Y2goJ3VybCcsIG5pbCkKICB1cmwuZW5jb2RlISg6eG1sID0+IDphdHRyKSB1bmxlc3MgdXJsLm5pbD8KICB5MCA9IHkKICBub2RlWydsYWJlbCddLmVhY2ggZG8gfGxpbmV8CiAgICBsaW5lLmVuY29kZSEoOnhtbCA9PiA6dGV4dCkKICAgIGlmIHVybC5uaWw/CiAgICAgIG91dC5wdXNoKCUoPHRleHQgI3t0ZXh0c3R5bGV9IHg9IiN7eH0iIHk9IiN7eTB9Ij4je2xpbmV9PC90ZXh0PikpCiAgICBlbHNlCiAgICAgIG91dC5wdXNoKCUoPGEgeGxpbms6aHJlZj0je3VybH0gdGFyZ2V0PSJfcGFyZW50Ij48dGV4dCAje2xpbmtzdHlsZX0geD0iI3t4fSIgeT0iI3t5MH0iPiN7bGluZX08L3RleHQ+PC9hPikpCiAgICBlbmQKICAgIHkwICs9IGxoICMgU2hpZnQgYmFzZWxpbmUgYnkgZnVsbCBsaW5lICsgc3BhY2luZyBoZWlnaHQuCiAgZW5kCmVuZAokcmVuZGVyLmRvYy5mZXRjaCgnZWRnZXMnLCBbXSkuZWFjaCBkbyB8ZWRnZXwKICBwYXRoID0gZWRnZS5mZXRjaCgncGF0aCcsIG5pbCkKICBuZXh0IGlmIHBhdGgubmlsPwogIHBhdGguZWFjaCBkbyB8cHwKICAgIHBbJ3hvJ10gPSBwWyd4byddLnRvX2kudG9fcwogICAgcFsneW8nXSA9IChoaCAtIHBbJ3lvJ10pLnRvX2kudG9fcwogIGVuZAogIGlmIHBhdGguc2l6ZSA9PSAyCiAgICBvdXQucHVzaCglKDxsaW5lICN7bGluZXN0eWxlfSB4MT0iI3twYXRoWzBdWyd4byddfSIgeDI9IiN7cGF0aFsxXVsneG8nXX0iIHkxPSIje3BhdGhbMF1bJ3lvJ119IiB5Mj0iI3twYXRoWzFdWyd5byddfSIvPikpCiAgZWxzZQogICAgcHRzID0gcGF0aC5tYXAgeyB8cHwgIiN7cFsneG8nXX0sI3twWyd5byddfSIgfQogICAgb3V0LnB1c2goJSg8cG9seWxpbmUgI3tsaW5lc3R5bGV9IHBvaW50cz0iI3twdHMuam9pbignICcpfSIvPikpCiAgZW5kCmVuZApvdXQuam9pbigiXG4iKQolPgo8L3N2Zz4K
2
+ styles:
3
+ diagram:
4
+ default:
5
+ edge_gap: 20
6
+ width_margin: 10
7
+ height_margin: 10
8
+ node:
9
+ default:
10
+ width_margin: 10
11
+ height_margin: 10
12
+ font_size: 16
13
+ font_ascend: 0.8
14
+ font_line_spacing: 0.2
15
+ font_height: 1
16
+ font_width: 0.5
17
+ font_fill: "#000000"
18
+ url_fill: "#000000"
19
+ fill: "#ffffff"
20
+ stroke: "#000000"
21
+ stroke_width: 2
22
+ size_estimator: |
23
+ $render.default_size($render.node['font_size'],
24
+ $render.node['font_width'], $render.node['font_height'],
25
+ $render.node['font_line_spacing'],
26
+ $render.node['width_margin'], $render.node['height_margin'],
27
+ $render.doc['diagram']['edge_gap'])
28
+ edge:
29
+ default:
30
+ stroke_width: 2
31
+ stroke: "#000000"
32
+ base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpoaCArPSAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnaGVpZ2h0X21hcmdpbicpCgpvdXQgPSBbCiAgJSg8c3ZnIHdpZHRoPSIje3cgKyAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnd2lkdGhfbWFyZ2luJyl9IiBoZWlnaHQ9IiN7aGh9IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiPikKXQokcmVuZGVyLmRvYy5mZXRjaCgnbm9kZXMnLCBbXSkuZWFjaCBkbyB8bm9kZXwKICB3ID0gbm9kZVsndyddLnRvX2kKICBoID0gbm9kZVsnaCddLnRvX2kKICB4ID0gbm9kZVsneG8nXS50b19pCiAgeSA9IGhoIC0gbm9kZVsneW8nXS50b19pIC0gaAogIG5vZGVzdHlsZSA9ICUoZmlsbD0iI3tub2RlWydmaWxsJ119IiBzdHJva2U9IiN7bm9kZVsnc3Ryb2tlJ119IiBzdHJva2Utd2lkdGg9IiN7bm9kZVsnc3Ryb2tlX3dpZHRoJ119IikKICBvdXQucHVzaCglKDxyZWN0ICN7bm9kZXN0eWxlfSBoZWlnaHQ9IiN7aH0iIHdpZHRoPSIje3d9IiB4PSIje3h9IiB5PSIje3l9Ii8+KSkKICB4ICs9IG5vZGVbJ3dpZHRoX21hcmdpbiddCiAgZnMgPSBub2RlWydmb250X3NpemUnXQogIGxoID0gZnMgKiAoMSArIG5vZGVbJ2ZvbnRfbGluZV9zcGFjaW5nJ10pCiAgeSArPSBub2RlWydoZWlnaHRfbWFyZ2luJ10gKyBmcyAqIG5vZGVbJ2ZvbnRfYXNjZW5kJ10gIyBCYXNlbGluZSBmb3IgZmlyc3QgbGluZS4KICB1cmwgPSBub2RlLmZldGNoKCd1cmwnLCBuaWwpCiAgdXJsLmVuY29kZSEoOnhtbCA9PiA6YXR0cikgdW5sZXNzIHVybC5uaWw/CiAgeTAgPSB5CiAgdGV4dHN0eWxlID0gJShmaWxsPSIje25vZGVbJ2ZvbnRfZmlsbCddfSIgZm9udC1mYW1pbHk9InNlcmlmIiBmb250LXNpemU9IiN7ZnN9IiBzdHJva2U9IiN7bm9kZVsnZm9udF9maWxsJ119IiBzdHJva2Utd2lkdGg9IjAiIHhtbDpzcGFjZT0icHJlc2VydmUiKQogIGxpbmtzdHlsZSA9ICUoZmlsbD0iI3tub2RlWyd1cmxfZmlsbCddfSIgZm9udC1mYW1pbHk9InNlcmlmIiBmb250LXNpemU9IiN7ZnN9IiBzdHJva2U9IiN7bm9kZVsndXJsX2ZpbGwnXX0iIHN0cm9rZS13aWR0aD0iMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIpCiAgbm9kZVsndGV4dCddLmVhY2ggZG8gfGxpbmV8CiAgICBsaW5lLmVuY29kZSEoOnhtbCA9PiA6dGV4dCkKICAgIGlmIHVybC5uaWw/CiAgICAgIG91dC5wdXNoKCUoPHRleHQgI3t0ZXh0c3R5bGV9IHg9IiN7eH0iIHk9IiN7eTB9Ij4je2xpbmV9PC90ZXh0PikpCiAgICBlbHNlCiAgICAgIG91dC5wdXNoKCUoPGEgeGxpbms6aHJlZj0je3VybH0gdGFyZ2V0PSJfcGFyZW50Ij48dGV4dCAje2xpbmtzdHlsZX0geD0iI3t4fSIgeT0iI3t5MH0iPiN7bGluZX08L3RleHQ+PC9hPikpCiAgICBlbmQKICAgIHkwICs9IGxoICMgU2hpZnQgYmFzZWxpbmUgYnkgZnVsbCBsaW5lICsgc3BhY2luZyBoZWlnaHQuCiAgZW5kCmVuZAokcmVuZGVyLmRvYy5mZXRjaCgnZWRnZXMnLCBbXSkuZWFjaCBkbyB8ZWRnZXwKICBsaW5lc3R5bGUgPSAlKGZpbGw9Im5vbmUiIHN0cm9rZT0iI3tlZGdlWydzdHJva2UnXX0iIHN0cm9rZS13aWR0aD0iI3tlZGdlWydzdHJva2Vfd2lkdGgnXX0iKQogIHBhdGggPSBlZGdlLmZldGNoKCdwYXRoJywgbmlsKQogIG5leHQgaWYgcGF0aC5uaWw/CiAgcGF0aC5lYWNoIGRvIHxwfAogICAgcFsneG8nXSA9IHBbJ3hvJ10udG9faS50b19zCiAgICBwWyd5byddID0gKGhoIC0gcFsneW8nXSkudG9faS50b19zCiAgZW5kCiAgaWYgcGF0aC5zaXplID09IDIKICAgIG91dC5wdXNoKCUoPGxpbmUgI3tsaW5lc3R5bGV9IHgxPSIje3BhdGhbMF1bJ3hvJ119IiB4Mj0iI3twYXRoWzFdWyd4byddfSIgeTE9IiN7cGF0aFswXVsneW8nXX0iIHkyPSIje3BhdGhbMV1bJ3lvJ119Ii8+KSkKICBlbHNlCiAgICBwdHMgPSBwYXRoLm1hcCB7IHxwfCAiI3twWyd4byddfSwje3BbJ3lvJ119IiB9CiAgICBvdXQucHVzaCglKDxwb2x5bGluZSAje2xpbmVzdHlsZX0gcG9pbnRzPSIje3B0cy5qb2luKCcgJyl9Ii8+KSkKICBlbmQKZW5kCm91dC5qb2luKCJcbiIpCiU+Cjwvc3ZnPgo=
data/template/root.yaml CHANGED
@@ -1,19 +1,30 @@
1
- defaults:
2
- edge_gap: 20
3
- width_margin: 10
4
- height_margin: 10
5
- font:
6
- font_size: 16
7
- stroke_width: 2
8
- width_key: w
9
- height_key: h
10
- size: default
11
- sizes:
12
- default: |
13
- $render.default_size(
14
- $render.node.fetch('width_scale', $render.defaults.font.width),
15
- $render.node.fetch('height_scale', $render.defaults.font.size),
16
- $render.node.fetch('line_spacing', $render.defaults.font.line_spacing),
17
- $render.node.fetch('width_margin', $render.defaults.width_margin),
18
- $render.node.fetch('height_margin', $render.defaults.height_margin),
19
- $render.node.fetch('edge_gap', $render.defaults.edge_gap))
1
+ styles:
2
+ diagram:
3
+ default:
4
+ edge_gap: 20
5
+ width_margin: 10
6
+ height_margin: 10
7
+ node:
8
+ default:
9
+ width_margin: 10
10
+ height_margin: 10
11
+ font_size: 16
12
+ font_ascend: 0.8
13
+ font_line_spacing: 0.2
14
+ font_height: 1
15
+ font_width: 0.5
16
+ font_fill: "#000000"
17
+ url_fill: "#000000"
18
+ fill: "#ffffff"
19
+ stroke: "#000000"
20
+ stroke_width: 2
21
+ size_estimator: |
22
+ $render.default_size($render.node['font_size'],
23
+ $render.node['font_width'], $render.node['font_height'],
24
+ $render.node['font_line_spacing'],
25
+ $render.node['width_margin'], $render.node['height_margin'],
26
+ $render.doc['diagram']['edge_gap'])
27
+ edge:
28
+ default:
29
+ stroke_width: 2
30
+ stroke: "#000000"
data/template/svg_1.1.erb CHANGED
@@ -1,33 +1,28 @@
1
1
  <?xml version="1.0"?>
2
2
  <%=
3
3
  w, hh = $render.dimensions
4
- hm = $render.defaults.height_margin
5
- hh += hm
6
- ma = $render.defaults.font.max_ascend
7
- fs = $render.defaults.font.size
8
- lh = fs + $render.defaults.font.line_spacing
9
- wm = $render.defaults.width_margin
10
- sw = $render.get_default('stroke_width', 5)
11
- linestyle = %(fill="none" stroke="#000000" stroke-width="#{sw}")
12
- textstyle = %(fill="#000000" font-family="serif" font-size="#{fs}" stroke="#000000" stroke-width="0" xml:space="preserve")
13
- linkstyle = %(fill="#2020ff" font-family="serif" font-size="#{fs}" stroke="#2020ff" stroke-width="0" xml:space="preserve")
4
+ hh += $render.doc.dig('diagram', 'height_margin')
14
5
 
15
6
  out = [
16
- %(<svg width="#{w + $render.defaults.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">)
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">)
17
8
  ]
18
9
  $render.doc.fetch('nodes', []).each do |node|
19
- w = node[$render.defaults.width_key].to_i
20
- h = node[$render.defaults.height_key].to_i
10
+ w = node['w'].to_i
11
+ h = node['h'].to_i
21
12
  x = node['xo'].to_i
22
13
  y = hh - node['yo'].to_i - h
23
- nodestyle = %(fill="#{node.fetch('fill', '#ffffff')}" stroke="#000000" stroke-width="#{sw}")
14
+ nodestyle = %(fill="#{node['fill']}" stroke="#{node['stroke']}" stroke-width="#{node['stroke_width']}")
24
15
  out.push(%(<rect #{nodestyle} height="#{h}" width="#{w}" x="#{x}" y="#{y}"/>))
25
- x += wm
26
- y += hm + ma # Baseline for first line.
16
+ x += node['width_margin']
17
+ fs = node['font_size']
18
+ lh = fs * (1 + node['font_line_spacing'])
19
+ y += node['height_margin'] + fs * node['font_ascend'] # Baseline for first line.
27
20
  url = node.fetch('url', nil)
28
21
  url.encode!(:xml => :attr) unless url.nil?
29
22
  y0 = y
30
- node['label'].each do |line|
23
+ textstyle = %(fill="#{node['font_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['font_fill']}" stroke-width="0" xml:space="preserve")
24
+ linkstyle = %(fill="#{node['url_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['url_fill']}" stroke-width="0" xml:space="preserve")
25
+ node['text'].each do |line|
31
26
  line.encode!(:xml => :text)
32
27
  if url.nil?
33
28
  out.push(%(<text #{textstyle} x="#{x}" y="#{y0}">#{line}</text>))
@@ -38,6 +33,7 @@ $render.doc.fetch('nodes', []).each do |node|
38
33
  end
39
34
  end
40
35
  $render.doc.fetch('edges', []).each do |edge|
36
+ linestyle = %(fill="none" stroke="#{edge['stroke']}" stroke-width="#{edge['stroke_width']}")
41
37
  path = edge.fetch('path', nil)
42
38
  next if path.nil?
43
39
  path.each do |p|
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.3.0
4
+ version: 0.4.1
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-01-27 00:00:00.000000000 Z
11
+ date: 2023-01-31 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14