svg_drawer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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