svg_drawer 1.0.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.
@@ -0,0 +1,171 @@
1
+ module SvgDrawer
2
+ class Polyline < Base
3
+ # NOTE: reposition and scale behaviors can be moved to Base
4
+ # but that is not needed at the moment
5
+ #
6
+ # :expand ensures that if the elem is smaller than the given dimensions,
7
+ # it will be scaled *up* until its either X or Y dim hits the bounds
8
+ # :shrink is similar, but scales the element *down* until both X and Y
9
+ # dim fit within the bounds
10
+ #
11
+ # If either :expand or :shrink are given, :overflow is ignored
12
+ #
13
+ # :scale_size is taken into account only if :shrink and/or :expand are
14
+ # given, and determines whether the given size (i.e. stroke-width) is
15
+ # also scaled accordingly
16
+ #
17
+ # :dotspace, if given, will cause the line to become dotted
18
+ #
19
+ defaults fill: 'none',
20
+ stroke: 'black',
21
+ linecap: 'butt',
22
+ linejoin: 'miter',
23
+ size: 1,
24
+ x_reposition: 'none', # none/left/center/right
25
+ y_reposition: 'none', # none/top/middle/bottom
26
+ expand: false,
27
+ shrink: false,
28
+ dotspace: 0,
29
+ overflow: false,
30
+ scale: 1,
31
+ scale_size: true
32
+
33
+ def initialize(points, params = {})
34
+ @points = points
35
+ super(params)
36
+ end
37
+
38
+ def width
39
+ param(:overflow) ?
40
+ param(:width, calc_width) :
41
+ [param(:width, 0), calc_width].max
42
+ end
43
+
44
+ def height
45
+ param(:overflow) ?
46
+ param(:height, calc_height) :
47
+ [param(:height, 0), calc_height].max
48
+ end
49
+
50
+ def incomplete
51
+ @points.size < 4 || @points.size.odd? ? self : nil
52
+ end
53
+
54
+ def min_x
55
+ @min_x ||= @points.each_slice(2).min_by(&:first).first - cap_size
56
+ end
57
+
58
+ def max_x
59
+ @max_x ||= @points.each_slice(2).max_by(&:first).first + cap_size
60
+ end
61
+
62
+ def min_y
63
+ @min_y ||= @points.each_slice(2).min_by(&:last).last - cap_size
64
+ end
65
+
66
+ def max_y
67
+ @max_y ||= @points.each_slice(2).max_by(&:last).last + cap_size
68
+ end
69
+
70
+ private
71
+
72
+ def _draw(parent)
73
+ size = param(:scale_size) ? param(:size) : param(:size) / scale
74
+ dotspace = param(:scale_size) ? param(:dotspace) : param(:dotspace) / scale
75
+ dotsize = size
76
+ style = {}
77
+
78
+ if param(:linecap).eql?('round')
79
+ dotsize = 0
80
+ dotspace *= 2
81
+ end
82
+
83
+ # need symbol keys due to a bug in Rasem::SVGTag#write_styles
84
+ style[:fill] = param(:fill)
85
+ style[:stroke] = param(:stroke)
86
+ style[:'stroke-width'] = size
87
+ style[:'stroke-linecap'] = param(:linecap)
88
+ style[:'stroke-linejoin'] = param(:linejoin)
89
+ style[:'stroke-dasharray'] = "#{dotsize}, #{dotspace}" if dotspace > 0
90
+
91
+ Utils::RasemWrapper.group(parent, class: 'polyline') do |polyline_group|
92
+ poly = polyline_group.polyline(@points, style: style.dup)
93
+ poly.translate(translate_x, translate_y).scale(scale, scale)
94
+ end
95
+ end
96
+
97
+ def calc_width
98
+ calc_width_unscaled * scale
99
+ end
100
+
101
+ def calc_height
102
+ calc_height_unscaled * scale
103
+ end
104
+
105
+ def calc_width_unscaled
106
+ max_x - min_x
107
+ end
108
+
109
+ def calc_height_unscaled
110
+ max_y - min_y
111
+ end
112
+
113
+ def width_unscaled
114
+ param(:overflow) ?
115
+ param(:width, calc_width_unscaled) :
116
+ [param(:width, 0), calc_width_unscaled].max
117
+ end
118
+
119
+ def height_unscaled
120
+ param(:overflow) ?
121
+ param(:height, calc_height_unscaled) :
122
+ [param(:height, 0), calc_height_unscaled].max
123
+ end
124
+
125
+ def scale
126
+ [scale_x, scale_y].min * param(:scale)
127
+ end
128
+
129
+ def scale_x
130
+ return 1 unless param(:width) && (param(:expand) || param(:shrink))
131
+ scale = param(:width).to_d / calc_width_unscaled
132
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
133
+ scale
134
+ end
135
+
136
+ def scale_y
137
+ return 1 unless param(:height) && (param(:expand) || param(:shrink))
138
+ scale = param(:height).to_d / calc_height_unscaled
139
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
140
+ scale
141
+ end
142
+
143
+ def translate_x
144
+ width_diff = (width - calc_width)
145
+
146
+ case param(:x_reposition)
147
+ when 'left' then -min_x * scale
148
+ when 'center' then -min_x * scale + width_diff / 2
149
+ when 'right' then -min_x * scale + width_diff
150
+ when 'none' then 0
151
+ else raise "Bad x_reposition: #{param(:x_reposition)}. Valid are: [left, right, center, none]"
152
+ end
153
+ end
154
+
155
+ def translate_y
156
+ height_diff = height - calc_height
157
+
158
+ case param(:y_reposition)
159
+ when 'top' then -min_y * scale
160
+ when 'middle' then -min_y * scale + height_diff / 2
161
+ when 'bottom' then -min_y * scale + height_diff
162
+ when 'none' then 0
163
+ else raise "Bad y_reposition: #{param(:y_reposition)}. Valid are: [top, bottom, middle, none]"
164
+ end
165
+ end
166
+
167
+ def cap_size
168
+ param(:size).to_d / 2
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,32 @@
1
+ module SvgDrawer
2
+ class BlankRow < Base
3
+ requires :columns
4
+ requires :width
5
+ requires :height
6
+
7
+ def incomplete
8
+ false
9
+ end
10
+
11
+ def width
12
+ @width ||= param(:width)
13
+ end
14
+
15
+ def height
16
+ @height ||= param(:height)
17
+ end
18
+
19
+ def cell_widths
20
+ Array.new(param(:columns), 0)
21
+ end
22
+
23
+ private
24
+
25
+ def _draw(parent, _col_widths)
26
+ Utils::RasemWrapper.group(parent, class: param(:class), id: param(:id)) do |row_group|
27
+ draw_border(row_group)
28
+ row_group.rectangle(0, 0, width, height, fill: 'none', stroke: 'none')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ module SvgDrawer
2
+ module Border
3
+ module_function
4
+
5
+ DEFAULT_STYLE = { stroke: 'black', size: 1 }.freeze
6
+
7
+ #
8
+ # Draw a rectangle with the given width and height
9
+ # The rectangle is actually 4 lines, with opacity of 1 or 0,
10
+ # depending on the values in the `borders` array.
11
+ # (e.g. [:left, :top])
12
+ #
13
+ # All lines share the same style, given by the border_style hash
14
+ # (see DEFAULT_STYLE for possible keys and their default values)
15
+ #
16
+ # For debugging purposes, lines can always be drawn even when there
17
+ # are no borders specified -- they are drawn transparent in this case.
18
+ # Drawing opacity=0 lines helps debugging in web inspector, but has
19
+ # some performance impact.
20
+ #
21
+ # @param parent [Rasem::SVGTagWithParent]
22
+ # @param width [Integer]
23
+ # @param height [Integer]
24
+ # @param borders [Array] (optional)
25
+ # @param border_style [Hash] (optional)
26
+ # @param svg_class [String] (optional)
27
+ # @param debug [Boolean] (optional) draw invisible borders
28
+ # @return [Rasem::SVGTagWithParent]
29
+ #
30
+ def draw(parent, width, height, borders, border_style, svg_class, debug)
31
+ return if !debug && (borders.nil? || borders.empty?)
32
+
33
+ style = DEFAULT_STYLE.merge(border_style || {})
34
+ style['stroke-width'] = style.delete(:size)
35
+ borders ||= []
36
+
37
+ line_points = {
38
+ top: [0, 0, width, 0],
39
+ right: [width, 0, width, height],
40
+ bottom: [width, height, 0, height],
41
+ left: [0, height, 0, 0]
42
+ }
43
+
44
+ klass = 'border'
45
+ klass.prepend("#{svg_class} ") if svg_class
46
+
47
+ Utils::RasemWrapper.group(parent, class: klass) do |group|
48
+ line_points.each do |border, points|
49
+ line_style = style.dup
50
+
51
+ # It is useful to draw an invisible border as this
52
+ # significantly helps debugging
53
+ unless borders.include?(border)
54
+ debug ? line_style[:opacity] = 0 : next
55
+ end
56
+
57
+ group.line(*points, line_style)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,65 @@
1
+ module SvgDrawer
2
+ class Cell < Base
3
+ def width
4
+ return @width if @width
5
+ ensure_complete!
6
+ @width = [param(:width, 0), @content.width].max
7
+ end
8
+
9
+ def height
10
+ return @height if @height
11
+ ensure_complete!
12
+ @height = [param(:height, 0), @content.height].max
13
+ end
14
+
15
+ def incomplete
16
+ @content.nil? ? self : @content.incomplete
17
+ end
18
+
19
+ def content(element = nil)
20
+ return @content unless element
21
+ raise TypeError, 'Argument must to respond to #draw' unless element.respond_to?(:draw)
22
+ element.update_params!(inherited: child_params)
23
+ @content = element
24
+ end
25
+
26
+ def text_box(text, params = {})
27
+ @content = TextBox.new(text, params.merge(inherited: child_params))
28
+ end
29
+
30
+ def path(path_components, params = {})
31
+ @content = Path.new(path_components, params.merge(inherited: child_params))
32
+ end
33
+
34
+ def polyline(points, params = {})
35
+ @content = Polyline.new(points, params.merge(inherited: child_params))
36
+ end
37
+
38
+ def multipolyline(strokes, params = {})
39
+ @content = Multipolyline.new(strokes, params.merge(inherited: child_params))
40
+ end
41
+
42
+ def line(points, params = {})
43
+ @content = Line.new(points, params.merge(inherited: child_params))
44
+ end
45
+
46
+ def circle(center, radius, params = {})
47
+ @content = Circle.new(center, radius, params.merge(inherited: child_params))
48
+ end
49
+
50
+ #
51
+ # See Row#draw for info on col_width and row_height
52
+ #
53
+ # @param parent [Rasem::SVGTagWithParent]
54
+ # @param col_width [Integer] Table-wide max colum width
55
+ # @param row_height [Integer] Table-wide max row height
56
+ # @return [Rasem::SVGTagWithParent]
57
+ #
58
+ def _draw(parent)
59
+ Utils::RasemWrapper.group(parent, class: param(:class), id: param(:id)) do |cell_group|
60
+ draw_border(cell_group)
61
+ @content.draw(cell_group, debug: @debug)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,127 @@
1
+ module SvgDrawer
2
+ class Row < Base
3
+ requires :columns
4
+
5
+ special :width # row width is not the same as cell width
6
+ special :columns # makes no sense for a cell
7
+ special :col_widths # makes no sense for a cell
8
+
9
+ def width
10
+ ensure_complete!
11
+ sum_width = cell_widths.reduce(&:+)
12
+ [param(:width, 0), sum_width].max
13
+ end
14
+
15
+ def height
16
+ ensure_complete!
17
+ [param(:height, 0), cell_heights.max].max
18
+ end
19
+
20
+ def incomplete
21
+ cells.size != param(:columns) ? self : find_incomplete_descendant
22
+ end
23
+
24
+ def cell_widths
25
+ ensure_complete!
26
+ cells.map(&:width)
27
+ end
28
+
29
+ def cell_heights
30
+ ensure_complete!
31
+ cells.map(&:height)
32
+ end
33
+
34
+ def col_widths
35
+ Table.col_widths(param(:col_widths), param(:width), param(:columns))
36
+ end
37
+
38
+ def cells
39
+ @cells ||= []
40
+ end
41
+
42
+ def add_cell(cell)
43
+ raise TypeError, "Expected Cell, got: #{cell.class}" unless cell.is_a?(Cell)
44
+ cell.update_params!(inherited: cell_params)
45
+ cells << cell
46
+ self
47
+ end
48
+
49
+ #
50
+ # @param params [Hash] cell params.
51
+ # @return [Row] self
52
+ #
53
+ def cell(params = {})
54
+ raise 'Cannot add more cells' unless incomplete
55
+ cell = Cell.new(params.merge(inherited: cell_params))
56
+ yield(cell)
57
+ cells << cell
58
+ self
59
+ end
60
+
61
+ def text_cell(text, params = {})
62
+ cell(params) { |c| c.text_box(text) }
63
+ end
64
+
65
+ def path_cell(path_components, params = {})
66
+ cell(params) { |c| c.path(path_components) }
67
+ end
68
+
69
+ def line_cell(points, params = {})
70
+ cell(params) { |c| c.line(points) }
71
+ end
72
+
73
+ def polyline_cell(points, params = {})
74
+ cell(params) { |c| c.polyline(points) }
75
+ end
76
+
77
+ def multipolyline_cell(strokes, params = {})
78
+ cell(params) { |c| c.multipolyline(strokes) }
79
+ end
80
+
81
+ def circle_cell(center, radius, params = {})
82
+ cell(params) { |c| c.circle(center, radius) }
83
+ end
84
+
85
+ private
86
+
87
+ #
88
+ # A note on cell widths:
89
+ # Cells are drawed not with their initial widths, but with the
90
+ # table-wide maximum width for the corresponding columns.
91
+ # This must happen at draw time, as we can't know what the max col
92
+ # width is until we have added all rows for the entire table.
93
+ #
94
+ # Similarly, for the heights:
95
+ # Cells are not drawed with their initial heigths, but with the
96
+ # row-wide maximum height.
97
+ # This must happen at draw time, as we can't know what the max cell
98
+ # height is until we have added all cells for this row.
99
+ #
100
+ # @param parent [Rasem::SVGTagWithParent]
101
+ # @param col_widths [Array] Table-wide max column widths
102
+ # @return [Rasem::SVGTagWithParent]
103
+ #
104
+ def _draw(parent, max_col_widths)
105
+ Utils::RasemWrapper.group(parent, class: param(:class), id: param(:id)) do |row_group|
106
+ draw_border(row_group, width_override: max_col_widths.reduce(&:+))
107
+
108
+ cells.zip(max_col_widths).reduce(0) do |x, (cell, col_width)|
109
+ cell.draw(row_group, debug: @debug).translate(x, 0)
110
+ x + col_width
111
+ end
112
+ end
113
+ end
114
+
115
+ def cell_params
116
+ return child_params unless col_widths && col_widths[cells.size]
117
+ child_params.merge(width: col_widths[cells.size])
118
+ end
119
+
120
+ def find_incomplete_descendant
121
+ cells.each.with_object(nil) do |cell, _|
122
+ res = cell.incomplete
123
+ break res if res
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,147 @@
1
+ module SvgDrawer
2
+ class Table < Base
3
+ requires :columns
4
+
5
+ special :height # table height is not the same as row height
6
+ special :row_height # makes no sense for a row
7
+
8
+ defaults col_widths: nil
9
+
10
+ #
11
+ # Infer col_widths from width (if needed)
12
+ #
13
+ def self.col_widths(col_widths, width, columns)
14
+ return if col_widths.nil? && width.nil?
15
+ sum_width = col_widths.reduce(&:+) if col_widths
16
+
17
+ if col_widths && width && sum_width != width
18
+ raise ArgumentError, "Sum of given col widths (#{col_widths}) doesn't match total element width (#{width})"
19
+ end
20
+
21
+ col_widths || Array.new(columns, width.to_d / columns)
22
+ end
23
+
24
+ def width
25
+ ensure_complete!
26
+ sum_width = col_widths ? col_widths.reduce(&:+) : 0
27
+ max_width = max_col_widths.reduce(&:+)
28
+
29
+ [sum_width, max_width].max
30
+ end
31
+
32
+ def height
33
+ ensure_complete!
34
+ sum_height = rows.reduce(0) { |a, e| a + e.height }
35
+ [param(:height, 0), sum_height].max
36
+ end
37
+
38
+ def incomplete
39
+ rows.none? ? self : find_incomplete_descendant
40
+ end
41
+
42
+ def rows
43
+ @rows ||= []
44
+ end
45
+
46
+ def add_row(row)
47
+ raise TypeError, "Expected Row, got: #{row.class}" unless row.is_a?(Row)
48
+ row.update_params!(inherited: row_params)
49
+ rows << row
50
+ end
51
+
52
+ #
53
+ # The params hash can contain a special :height value
54
+ # It will be used instead of the @row_height when creating the row
55
+ #
56
+ # @param params [Hash] row params
57
+ # @return [Table] self
58
+ #
59
+ def row(params = {})
60
+ row = Row.new(params.merge(inherited: row_params))
61
+ yield(row)
62
+ rows << row
63
+ self
64
+ end
65
+
66
+ def text_row(texts, params = {})
67
+ texts = texts.nil? ? [nil] : Array(texts)
68
+ row(params) { |r| texts.each { |text| r.text_cell(text) } }
69
+ end
70
+
71
+ def path_row(path_components, params = {})
72
+ row(params) { |r| r.path_cell(path_components) }
73
+ end
74
+
75
+ def line_row(points, params = {})
76
+ row(params) { |r| r.line_cell(points) }
77
+ end
78
+
79
+ def polyline_row(points, params = {})
80
+ row(params) { |r| r.polyline_cell(points) }
81
+ end
82
+
83
+ def multipolyline_row(strokes, params = {})
84
+ row(params) { |r| r.multipolyline_cell(strokes) }
85
+ end
86
+
87
+ def circle_row(center, radius, params = {})
88
+ row(params) { |r| r.circle_cell(center, radius) }
89
+ end
90
+
91
+ def sub_table_row(params = {})
92
+ t = Table.new(params)
93
+ row { |r| r.cell { |c| c.content(t) && yield(t) } }
94
+ end
95
+
96
+ def blank_row(params = {})
97
+ raise ArgumentError, ':height required' if !param(:row_height) && !params[:height]
98
+ rows << BlankRow.new(params.merge(inherited: row_params))
99
+ self
100
+ end
101
+
102
+ def col_widths
103
+ Table.col_widths(param(:col_widths), param(:width), param(:columns))
104
+ end
105
+
106
+ private
107
+
108
+ #
109
+ # The dimension overrides given when the table is actually
110
+ # the child of a (parent) table cell.
111
+ # In this case the overrides are used to draw proper borders
112
+ # (since the Cell element of the parent determines its)
113
+ #
114
+ # @param parent [Rasem::SVGTagWithParent]
115
+ # @param width_override [Integer] (optional) container width
116
+ # @param height_override [Integer] (optional) container height
117
+ # @return [Rasem::SVGTagWithParent]
118
+ #
119
+ def _draw(parent)
120
+ Utils::RasemWrapper.group(parent, class: param(:class), id: param(:id)) do |table_group|
121
+ draw_border(table_group)
122
+
123
+ rows.reduce(0) do |y, row|
124
+ row.draw(table_group, max_col_widths, debug: @debug).translate(0, y)
125
+ y + row.height
126
+ end
127
+ end
128
+ end
129
+
130
+ def row_params
131
+ param(:row_height)
132
+ return child_params unless param(:row_height)
133
+ child_params.merge(height: param(:row_height))
134
+ end
135
+
136
+ def find_incomplete_descendant
137
+ rows.each.with_object(nil) do |row, _|
138
+ res = row.incomplete
139
+ break res if res
140
+ end
141
+ end
142
+
143
+ def max_col_widths
144
+ rows.map(&:cell_widths).transpose.map(&:max)
145
+ end
146
+ end
147
+ end