diagrammatron 0.3.0 → 0.4.0

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: 32bd6ca5ad8db808b0b6c921ee0efe5b8e1a6befe4e535636f16ff98a22de139
4
+ data.tar.gz: c00027483fc54a9fab024c1e42a4e10e27b797e52e3a9217319a50542926a894
5
5
  SHA512:
6
- metadata.gz: '081df6de2fb3489cc0976a2805c829fb846ce5561617b15d7446dda4a6bfd0a63bbc9abf234e89e895ed90f710f756cbc8b1f47618ee4216e89a2c4e522e9046'
7
- data.tar.gz: 85e77e7adf696cd3f46fe2b41a77266d67cf1d7e904cb1c9eb4f83bae6e3fef8db7f199313923e588c8802e8edb6d8148869f2c8ef93685aeb3af48dc6a83252
6
+ metadata.gz: 7723bc76dfd7463a864319421074d8c17357f6c88a309efd81919c165ad836f876d48e062bf618b45e2b7fe779b1c564afaedfa6b105ae1fee9f84e5f52003d5
7
+ data.tar.gz: ed23e22fd6476e33afb4360cce23cefb0373dbd2c763611d9f61d0a20a283fe2dcd7a4bac6f137d628020db86819aacdea6387c889e5821a3ed4b958a5a2a736
@@ -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,98 @@ 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
+ styles.each do |name|
102
+ ns = mapping.fetch(name, nil)
103
+ raise "No such #{type_name} style: #{name} in #{mapping.keys.join(', ')}" if ns.nil?
104
+ s.merge! ns
105
+ end
106
+ # Keep values specified explicitly.
107
+ item.merge!(s) {|key, existing, from_template| existing }
108
+ end
109
+
110
+ def apply_node_styles(node)
111
+ fill(@n, 'node', node)
91
112
  end
92
- end
93
113
 
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)
114
+ def apply_edge_styles(edge)
115
+ fill(@e, 'edge', edge)
116
+ end
117
+
118
+ def apply_diagram_styles(diagram)
119
+ fill(@d, 'diagram', diagram)
106
120
  end
107
121
  end
108
122
 
109
123
  class SizeEstimation
110
- attr_accessor :node, :template, :ckd2count, :defaults
124
+ attr_accessor :node, :ckd2count, :doc
111
125
 
112
- def initialize(template, ckd2count, defaults)
126
+ def initialize(ckd2count, doc)
113
127
  @node = nil
114
- @template = template
115
128
  @ckd2count = ckd2count
116
- @defaults = defaults
129
+ @doc = doc
117
130
  end
118
131
 
119
- def get_binding
132
+ def exposed_binding
120
133
  binding
121
134
  end
122
135
 
123
- def get_default(key, default_value = nil)
124
- @template.fetch('defaults', {}).fetch(key, default_value)
125
- end
126
-
127
136
  def max_edges(key, edge_gap)
128
137
  c = [ @node['xo'], @node['yo'], key, -1 ]
129
138
  count = @ckd2count[c]
130
139
  c[3] = 1
131
140
  count = [ count, @ckd2count[c] ].max
132
141
  return 0 if count < 2
133
- (count - 1) * (edge_gap || @defaults.edge_gap)
142
+ (count - 1) * edge_gap
134
143
  end
135
144
 
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
145
+ # font_size is the actual size.
146
+ # font_width, font_height, font_line_spacing are [0, 1] size scaling factors.
147
+ # width_margin, height_margin are in same units as fonti, space inside node.
148
+ # edge_gap is minimum space between edges at any node side.
149
+ def default_size(font_size, font_width, font_height, font_line_spacing,
150
+ width_margin, height_margin, edge_gap)
151
+ lines = @node['text']
152
+ w = 2 * width_margin + font_width * font_size * (lines.map &(:size)).max
153
+ @node['w'] = [ w, max_edges('xo', edge_gap) ].max
154
+ h = 2 * height_margin + font_height * font_size * lines.size +
155
+ font_line_spacing * font_size * (lines.size - 1)
156
+ @node['h'] = [ h, max_edges('yo', edge_gap) ].max
146
157
  end
147
158
  end
148
159
 
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', {})
160
+ def estimate_sizes(doc, ckd2count)
161
+ $render = SizeEstimation.new(ckd2count, doc)
153
162
  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
163
  $render.node = node
164
+ label = node.fetch('label', 'unnamed')
158
165
  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
166
+ code = node.fetch('size_estimator',
167
+ %(raise NotImplementedError, "No size estimator for style: #{style}"))
164
168
  code = code.join("\n") if code.is_a? Array
165
169
  begin
166
- eval(code, $render.get_binding)
170
+ eval(code, $render.exposed_binding)
167
171
  rescue StandardError => e
168
172
  return aargh("Size estimate style #{style} node #{label} error #{e}", false)
169
173
  end
@@ -172,20 +176,20 @@ def estimate_sizes(doc, template, ckd2count, defaults)
172
176
  true
173
177
  end
174
178
 
175
- def maxima(doc, defaults)
179
+ def maxima(doc)
176
180
  xmax = Hash.new(0)
177
181
  ymax = Hash.new(0)
178
182
  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
183
+ xmax[node['xo']] = [ node['w'], xmax[node['xo']] ].max
184
+ ymax[node['yo']] = [ node['h'], ymax[node['yo']] ].max
181
185
  end
182
186
  [ xmax, ymax ]
183
187
  end
184
188
 
185
- def apply_maxima(doc, xmax, ymax, defaults)
189
+ def apply_maxima(doc, xmax, ymax)
186
190
  doc.fetch('nodes', []).each do |node|
187
- node[defaults.width_key] = xmax[node['xo']]
188
- node[defaults.height_key] = ymax[node['yo']]
191
+ node['w'] = xmax[node['xo']]
192
+ node['h'] = ymax[node['yo']]
189
193
  end
190
194
  end
191
195
 
@@ -200,8 +204,8 @@ def parallel_edge_step_minima(coords)
200
204
  c2m
201
205
  end
202
206
 
203
- def remap_coordinates(coords, cmax, c2min, defaults)
204
- c = defaults.edge_gap
207
+ def remap_coordinates(coords, cmax, c2min, edge_gap)
208
+ c = edge_gap
205
209
  gap = 0 # How much space all edge segments need.
206
210
  zero_after_decrease = false
207
211
  prev_dir = -2
@@ -210,19 +214,19 @@ def remap_coordinates(coords, cmax, c2min, defaults)
210
214
  case coord.direction
211
215
  when -1
212
216
  c += gap if -1 < prev_dir
213
- gap = defaults.edge_gap
217
+ gap = edge_gap
214
218
  coord.object[coord.key] = c
215
219
  when 0
216
- gap = defaults.edge_gap / c2min[coord.integer]
220
+ gap = edge_gap / c2min[coord.integer]
217
221
  if zero_after_decrease
218
222
  # Edge segment is at same range as nodes.
219
223
  coord.object[coord.key] = c + coord.fraction * cmax[coord.integer]
220
224
  else
221
225
  coord.object[coord.key] =
222
- c + (defaults.edge_gap * coord.fraction) / c2min[coord.integer]
226
+ c + (edge_gap * coord.fraction) / c2min[coord.integer]
223
227
  end
224
228
  when 1
225
- gap = defaults.edge_gap
229
+ gap = edge_gap
226
230
  c += cmax[coord.integer] unless prev_dir == 1
227
231
  coord.object[coord.key] = c
228
232
  zero_after_decrease = false
@@ -232,28 +236,23 @@ def remap_coordinates(coords, cmax, c2min, defaults)
232
236
  end
233
237
 
234
238
  class Render
235
- attr_accessor :doc, :template, :defaults
239
+ attr_accessor :doc, :template
236
240
 
237
- def initialize(doc, template, defaults)
241
+ def initialize(doc, template)
238
242
  @doc = doc
239
243
  @template = template
240
- @defaults = defaults
241
244
  end
242
245
 
243
- def get_binding
246
+ def exposed_binding
244
247
  binding
245
248
  end
246
249
 
247
- def get_default(key, default_value = nil)
248
- @template.fetch('defaults', {}).fetch(key, default_value)
249
- end
250
-
251
250
  def dimensions
252
251
  w = 0
253
252
  h = 0
254
253
  @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
254
+ w = [ w, node['xo'] + node['w'] ].max
255
+ h = [ h, node['yo'] + node['h'] ].max
257
256
  end
258
257
  @doc.fetch('edges', []).each do |edge|
259
258
  path = edge.fetch('path', nil)
@@ -267,9 +266,9 @@ class Render
267
266
  end
268
267
  end
269
268
 
270
- def apply(doc, template, defaults)
271
- $render = Render.new(doc, template, defaults)
272
- out = ERB.new(template.fetch('template', '')).result($render.get_binding)
269
+ def apply(doc, template)
270
+ $render = Render.new(doc, template)
271
+ out = ERB.new(template.fetch('template', '')).result($render.exposed_binding)
273
272
  $render = nil
274
273
  out
275
274
  end
@@ -278,6 +277,7 @@ def main
278
277
  template = nil
279
278
  input = nil
280
279
  output = nil
280
+ styles = nil
281
281
  parser = OptionParser.new do |opts|
282
282
  opts.summary_indent = ' '
283
283
  opts.summary_width = 20
@@ -317,29 +317,37 @@ Output is the file produced by the erb-template.
317
317
  return aargh("Key #{key} base-64 decoding failed to key #{nk}", 2)
318
318
  end
319
319
  end
320
- defaults = Defaults.new(template)
321
320
 
322
321
  doc = load_source(input)
323
322
  return 2 if doc.nil?
324
323
 
324
+ styles = Styles.new(template.fetch('styles', {}), doc.fetch('styles', {}))
325
+ doc.fetch('nodes', []).each do |node|
326
+ styles.apply_node_styles(node)
327
+ node['text'] = node.fetch('text', node.fetch('label', '')).split("\n")
328
+ end
329
+ doc.fetch('edges', []).each { |edge| styles.apply_edge_styles(edge) }
330
+ doc['diagram'] = {} unless doc.key? 'diagram'
331
+ styles.apply_diagram_styles(doc['diagram'])
332
+
325
333
  begin
326
334
  xcoords, ycoords, ckd2count = separate_coordinates(doc)
327
335
  rescue StandardError
328
336
  return aargh('Error processing input.', 3)
329
337
  end
330
338
 
331
- return 4 unless estimate_sizes(doc, template, ckd2count, defaults)
339
+ return 4 unless estimate_sizes(doc, ckd2count)
332
340
 
333
341
  # 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)
342
+ xmax, ymax = maxima(doc)
343
+ apply_maxima(doc, xmax, ymax)
336
344
 
337
345
  x2min = parallel_edge_step_minima(xcoords)
338
346
  y2min = parallel_edge_step_minima(ycoords)
339
- remap_coordinates(xcoords, xmax, x2min, defaults)
340
- remap_coordinates(ycoords, ymax, y2min, defaults)
347
+ remap_coordinates(xcoords, xmax, x2min, doc.dig('diagram', 'edge_gap'))
348
+ remap_coordinates(ycoords, ymax, y2min, doc.dig('diagram', 'edge_gap'))
341
349
 
342
- dump_result(output, apply(doc, template, defaults), 5)
350
+ dump_result(output, apply(doc, template), 5)
343
351
  end
344
352
 
345
353
  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.0
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