vibes-rb 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f27d3143c9c22784f9d2b3670d66e905dfd0c923acffe0934cd0c5858865d0c4
4
+ data.tar.gz: dfb4418bd7415f16ec3b1a4bb702dd9ad113c5fa51babf0ab3adea2efa791617
5
+ SHA512:
6
+ metadata.gz: 397ab399abc397281cfde397b4e349da431e5b7e99e1faebc8a14faf51f3ba2699133e8291d340351b00e02b08f2fa7c5ed6119f7c412773cac6baac1f715dcf
7
+ data.tar.gz: b595190c727f6bf3f4a2a715e0affda322984e5dbf75ba8f6e5eedab339b6c68dac7e98ef3c70cd45773c5661d8c198e7ea613f9ae64a36aea8a8c71ed9a281e
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --readme README.md
2
+ --title 'VIBes Documentation'
3
+ --verbose
4
+ --charset utf-8
5
+ --markup markdown
6
+ --default-return self
7
+ 'lib/**/*.rb'
data/LICENSE ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+
2
+ VIBes
3
+ =====
4
+
5
+ This gem is a Ruby API to connect to the VIBes viewer (Visualizer for Intervals and Boxes).
6
+
7
+ You will need the VIBes viewer executable.
8
+ See <https://github.com/ENSTABretagneRobotics/VIBES> to get and install it.
9
+
10
+
11
+ Install the vibes-rb gem
12
+ ------------------------
13
+
14
+ ~~~bash
15
+ sudo gem install vibes-rb
16
+ ~~~
17
+
18
+
19
+ Install from sources
20
+ --------------------
21
+
22
+ ~~~bash
23
+ git clone git@gitlab.ensieta.ecole:bollenth/vibes.rb.git
24
+ cd vibes.rb
25
+ gem build vibes-rb.gemspec
26
+ gem install vibes-rb-0.2.3.gem
27
+ ~~~
28
+
29
+
30
+ Documentation
31
+ -------------
32
+
33
+ Documentation is available at <https://www.rubydoc.info/gems/vibes-rb>
34
+
35
+ You can build the documentation using yard:
36
+
37
+ ~~~bash
38
+ gem install yard
39
+ cd vibes.rb
40
+ yard
41
+ firefox doc/index.html
42
+ ~~~
43
+
@@ -0,0 +1,3 @@
1
+ module VIBes
2
+ VERSION = '0.2.3'
3
+ end
data/lib/vibes-rb.rb ADDED
@@ -0,0 +1,759 @@
1
+ # Copyright (C) 2020, 2022 Théotime Bollengier <theotime.bollengier@ensta-bretagne.fr>
2
+ #
3
+ # This file is part of vibes.rb
4
+ #
5
+ # vibes.rb is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # vibes.rb is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with vibes.rb. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+
19
+ require 'json'
20
+ require_relative 'vibes-rb/version.rb'
21
+
22
+
23
+ # The {VIBes} module handles communication with the [VIBes viewer](https://github.com/ENSTABretagneRobotics/VIBES) executable.
24
+ module VIBes
25
+ # File used to communicate with the viewer in the JSON format
26
+ # @return [File, nil]
27
+ # @!visibility private
28
+ @@vibes_channel = nil
29
+
30
+
31
+ # Open communication with the VIBes viewer.
32
+ # If the viewer is not executing, forks to launch it in a new process.
33
+ #
34
+ # Normaly the user do not have to call this function,
35
+ # it is automatically called when the first message is being sent
36
+ # to the viewer.
37
+ #
38
+ # It is only useful when the user wants to save the commands in a
39
+ # specific file instead of sending them to the viewer.
40
+ # In this case, this method should be called before any figure method is called.
41
+ # @example
42
+ # VIBes.begin_drawing('titi.json')
43
+ # f = VIBes::Figure.new
44
+ # f.width = 500
45
+ # f.height = 250
46
+ # f.draw_box([-2, 3], [-1, 4], color: 'k[r]')
47
+ # VIBes.end_drawing
48
+ # puts File.read('titi.json')
49
+ # # {"action":"new","figure":"default"}
50
+ # # {"action":"set","figure":"default","properties":{"width":500}}
51
+ # # {"action":"set","figure":"default","properties":{"height":500}}
52
+ # # {"action":"draw","figure":"default","shape":{"format":"k[r]","type":"box","bounds":[-2.0,3.0,-1.0,4.0]}}
53
+ # @param fname [String, nil] name of the file used to exchange data with the viewer
54
+ def self.begin_drawing(fname = nil)
55
+ if fname.kind_of?(String) and not(fname.empty?) then
56
+ @@vibes_channel.close if @@vibes_channel
57
+ @@vibes_channel = File.open(fname, 'a')
58
+ else
59
+ user_dir = ENV['USERPROFILE'] # windows
60
+ user_dir = ENV['HOME'] if user_dir.nil?
61
+ if user_dir then # Environment variable found, connect to a file in user's profile directory
62
+ fname = File.join(user_dir, '.vibes.json')
63
+ unless File.exist?(fname) then
64
+ Process.detach(fork{Process.exec('VIBes-viewer')})
65
+ startTime = Time.now
66
+ until File.exist?(fname) do
67
+ sleep 0.1
68
+ if (Time.now - startTime) > 1 then
69
+ warn 'VIBes-viewer process does not seem to be started'
70
+ break
71
+ end
72
+ end
73
+ end
74
+ begin_drawing(fname)
75
+ else # Connect to a file in working directory
76
+ begin_drawing('vibes.json')
77
+ end
78
+ end
79
+ self
80
+ end
81
+
82
+
83
+ # Close the communication channel with the viewer.
84
+ #
85
+ # In most cases, users don't have to call this method.
86
+ # It is only useful when commands are saved in a specific file
87
+ # instead of being send to the VIBes viewer.
88
+ # @see begin_drawing
89
+ def self.end_drawing
90
+ if @@vibes_channel then
91
+ @@vibes_channel.close
92
+ @@vibes_channel = nil
93
+ end
94
+ self
95
+ end
96
+
97
+
98
+ # Send a message to the viewer.
99
+ # Initiate the viewer via {begin_drawing}
100
+ # if the communication channel is not yet opened.
101
+ # @!visibility private
102
+ def self.msg(str)
103
+ begin_drawing unless @@vibes_channel
104
+ @@vibes_channel.write str
105
+ @@vibes_channel.flush
106
+ self
107
+ end
108
+
109
+
110
+ # {Figure} is the class used to handle a single figure with the viewer.
111
+ class Figure
112
+ # Name of the figure that will appear on the viewer.
113
+ # If two instances of {Figure} have the same name,
114
+ # they will treat the same figure on the viewer.
115
+ # @return [String] the figure name
116
+ attr_reader :name
117
+
118
+
119
+ # @!group Figure management
120
+
121
+
122
+ # Create a new figure name _figname_.
123
+ # @example
124
+ # f = VIBes::Figure.new 'Name of the figure'
125
+ # @param figure_name [String] the name of the figure
126
+ # @return [Figure]
127
+ def initialize(figure_name = 'default')
128
+ figure_name = 'default' if not(figure_name.is_a?(String)) or figure_name.empty?
129
+ @name = figure_name
130
+ VIBes.msg "{\"action\":\"new\",\"figure\":\"#{@name}\"}\n\n"
131
+ end
132
+
133
+
134
+ # Close the figure
135
+ def close
136
+ VIBes.msg "{\"action\":\"close\",\"figure\":\"#{@name}\"}\n\n"
137
+ self
138
+ end
139
+
140
+
141
+ # Clear the content of the figure
142
+ def clear
143
+ VIBes.msg "{\"action\":\"clear\",\"figure\":\"#{@name}\"}\n\n"
144
+ self
145
+ end
146
+
147
+
148
+ # Save the figure on disk.
149
+ # Available formats are: png, jpeg, bmp and svg.
150
+ # @param file_name [String] path of the file to save the figure into.
151
+ def save_image(file_name = nil, **kwargs)
152
+ file_name = '' unless file_name.is_a?(String) and file_name.length > 0
153
+ kwargs[:action] = :export
154
+ kwargs[:figure] = @name
155
+ kwargs[:file] = File.expand_path(file_name)
156
+ VIBes.msg "#{kwargs.to_json}\n\n"
157
+ self
158
+ end
159
+
160
+
161
+ # Set figure properties using keyword arguments.
162
+ # @example
163
+ # # Set a new position and viewport
164
+ # fig.set_properties(x: 100, y: 50, width: 500, height: 250)
165
+ # @see []=
166
+ # @see method_missing
167
+ def set_properties(**kwargs)
168
+ VIBes.msg "{\"action\":\"set\",\"figure\":\"#{@name}\",\"properties\":#{kwargs.to_json}}\n\n"
169
+ self
170
+ end
171
+
172
+
173
+ # Set a property using the form [property]=value
174
+ # @example
175
+ # # Set the new x position
176
+ # fig[:x] = 100
177
+ # @see set_properties
178
+ # @see method_missing
179
+ def []=(key, value)
180
+ set_properties(**{key.to_sym => value})
181
+ end
182
+
183
+
184
+ # Set a property using the form `self.property=value`.
185
+ # If the method is not known to the figure and is in the form 'figure.method=val',
186
+ # call the {set_properties} method whith the method name as key _val_ as value.
187
+ # @example
188
+ # f = VIBes::Figure.new
189
+ # f.width = 500
190
+ # f.height = 250
191
+ # # width= and height= are not instance methods of Figure,
192
+ # # so the last two method calls are translated into
193
+ # # f.set_properties(width: 500)
194
+ # # f.set_properties(height: 250)
195
+ # @see set_properties
196
+ # @see []=
197
+ def method_missing(m, *args, &block)
198
+ if m.to_s =~ /=$/ then
199
+ raise 'not expecting a block' if block
200
+ raise 'expecting 1 and only 1 argument' if args.length != 1
201
+ self[m.to_s.sub(/=$/, '')] = args.first
202
+ else
203
+ super
204
+ end
205
+ self
206
+ end
207
+
208
+
209
+ # Set the size of the figure
210
+ # @param width [Integer]
211
+ # @param height [Integer]
212
+ def set_size(width, height)
213
+ set_properties(width: width.to_i, height: height.to_i)
214
+ end
215
+
216
+
217
+ # Set the position of the figure window on the screen
218
+ # @param x [Float] horizontal axis position
219
+ # @param y [Float] vertical axis position
220
+ def set_pos(x, y)
221
+ set_properties(x: x.to_i, y: y.to_i)
222
+ end
223
+
224
+
225
+ # @!group Axis management
226
+
227
+
228
+ # Set axes limits to the bounding box of the drawing.
229
+ # @see axis_equal
230
+ def axis_auto
231
+ set_properties(viewbox: :auto)
232
+ end
233
+
234
+
235
+ # Same as {#axis_auto} but with the same ratio on the two axis.
236
+ # @see axis_auto
237
+ def axis_equal
238
+ set_properties(viewbox: :equal)
239
+ end
240
+
241
+
242
+ # Set the rectangle to be displayed.
243
+ # @overload axis_limits(x_min, x_max, y_min, y_max)
244
+ # @param [Float, Integer]
245
+ # @overload axis_limits(x_range, y_range)
246
+ # @param [Range<Float, Integer>]
247
+ # @overload axis_limits(x_interval, y_interval)
248
+ # @param [Interval] an object that respond to _lower_bound_ and _upper_bound_
249
+ # @overload axis_limits(xy_box)
250
+ # @param [IntervalVector] an interval vector of 2 intervals
251
+ def axis_limits(*v)
252
+ a = []
253
+ v.each do |e|
254
+ case e
255
+ when Numeric
256
+ a << e.to_f
257
+ when Range
258
+ l = e.begin.to_f
259
+ u = e.end.to_f
260
+ l, u = [u, l] if l > u
261
+ a << l
262
+ a << u
263
+ when Array
264
+ raise "array must have 1 or 2 elements" if e.length < 1 or e.length > 2
265
+ l = e.first.to_f
266
+ u = e.last.to_f
267
+ l, u = [u, l] if l > u
268
+ a << l
269
+ a << u
270
+ else
271
+ if VIBes::object_is_an_interval?(e)
272
+ a << e.lower_bound
273
+ a << e.upper_bound
274
+ elsif VIBes::object_is_a_box?(e)
275
+ raise "the box is expected to have 2 dimensions, not #{e.length}" if e.length != 2
276
+ a << e[0].lower_bound
277
+ a << e[0].upper_bound
278
+ a << e[1].lower_bound
279
+ a << e[1].upper_bound
280
+ else
281
+ raise "expecting floats, ranges, 2 element arrays, intervals or box, not #{e.class}"
282
+ end
283
+ end
284
+ end
285
+ raise "Should have 4 coordinates, not #{a.length}" if a.length != 4
286
+ set_properties(viewbox: a)
287
+ end
288
+
289
+
290
+ # Set axis labels
291
+ # @overload axis_labels(x_label, y_label)
292
+ # @param [String]
293
+ def axis_labels(*labels)
294
+ set_properties(axislabels: labels.collect!{|o| o.to_s})
295
+ end
296
+
297
+
298
+ # @!group Groups
299
+
300
+
301
+ # Create a new group with the specified group name and parameters.
302
+ #
303
+ # Groups can be created to gather objects which share common properties (color, ...).
304
+ # Then objects can be added to groups by using the _group:_ keyword.
305
+ # @param group_name [String] name of the group
306
+ # @param kwargs optional keyword arguments
307
+ # @example
308
+ # f = VIBes::Figure.new 'test groups'
309
+ # f.new_group('my group', format: 'r[darkBlue]')
310
+ # f.draw_circle([0, 0], 3, group: 'my group')
311
+ # f.draw_box(3..6, 0..3, group: 'my group')
312
+ def new_group(group_name, **kwargs)
313
+ kwargs = {type: :group, name: group_name.to_s}
314
+ kwargs.each{|k, v| kwargs[k.to_sym] = v}
315
+ draw kwargs
316
+ end
317
+
318
+
319
+ # Clear the content of the group _group_name_ from the figure.
320
+ # @param group_name [String] name of the group to remove
321
+ # @param kwargs optional keyword arguments
322
+ def clear_group(group_name, **kwargs)
323
+ kwargs[:action] = :clear
324
+ kwargs[:figure] = @name
325
+ kwargs[:group] = group_name.to_s
326
+ VIBes.msg "#{kwargs.to_json}\n\n"
327
+ self
328
+ end
329
+
330
+
331
+ # @!group Drawing functions
332
+
333
+
334
+ # Draw a box
335
+ # @param a [Numeric, Range, Array, Interval, IntervalVector] bounds of the box
336
+ # @param color [String] color format
337
+ # @param kwargs optional keyword arguments
338
+ # @example Draw a box with 𝒙 ∈ [0, 1] and 𝒚 ∈ [2, 3]
339
+ # f = VIBes::Figure.new
340
+ # # Using 4 numeric bounds
341
+ # f.draw_box(0, 1, 2, 3, color: 'k[r]')
342
+ # # Using 2 ranges
343
+ # f.draw_box(0..1, 2..3, color: 'k[b]')
344
+ # # Using 2 arrays
345
+ # f.draw_box([0, 1], [2, 3], color: 'k[g]')
346
+ # # Using 2 intervals
347
+ # x = P1788::Interval[0, 1]
348
+ # y = P1788::Interval[2, 3]
349
+ # f.draw_box(x, y, color: 'k[y]')
350
+ # # Using an interval vector
351
+ # b = P1788::IntervalVector[x, y]
352
+ # f.draw_box(b, color: 'k[c]')
353
+ def draw_box(*a, color: nil, **kwargs)
354
+ c = []
355
+ f = nil
356
+ a.each do |e|
357
+ case e
358
+ when Numeric
359
+ c << e.to_f
360
+ when Range
361
+ l = e.begin.to_f
362
+ u = e.end.to_f
363
+ l, u = [u, l] if l > u
364
+ c << l
365
+ c << u
366
+ when Array
367
+ raise "array must have 1 or 2 elements" if e.length < 1 or e.length > 2
368
+ l = e.first.to_f
369
+ u = e.last.to_f
370
+ l, u = [u, l] if l > u
371
+ c << l
372
+ c << u
373
+ when String
374
+ f = e
375
+ else
376
+ if VIBes::object_is_an_interval?(e) then
377
+ c << e.lower_bound
378
+ c << e.upper_bound
379
+ elsif VIBes::object_is_a_box?(e) then
380
+ e.each do |v|
381
+ c << v.lower_bound
382
+ c << v.upper_bound
383
+ end
384
+ else
385
+ raise "expecting floats, ranges, 2 element arrays, intervals or boxes, not #{e.class}"
386
+ end
387
+ end
388
+ end
389
+ raise "Should have a multiple of 2 coordinates, not #{c.length}" if c.length % 2 != 0
390
+ kwargs[:format] = f if f
391
+ kwargs[:format] = color if color.is_a?(String)
392
+ kwargs[:type] = :box
393
+ kwargs[:bounds] = c
394
+ draw(kwargs)
395
+ end
396
+
397
+
398
+ # Draw multiple boxes at once
399
+ # @param boxes [IntervalVector, Array<IntervalVector>] Boxes or an array of boxes
400
+ def draw_boxes(*boxes, color: nil, **kwargs)
401
+ kwargs[:bounds] = []
402
+ boxes.flatten!
403
+ color = boxes.select{|v| v.is_a?(String)}.first if color.nil?
404
+ boxes.reject!{|v| v.is_a?(String)}
405
+ boxes.each do |b|
406
+ if VIBes::object_is_a_box?(b) then
407
+ ba = []
408
+ b.each do |v|
409
+ ba << v.lower_bound
410
+ ba << v.upper_bound
411
+ end
412
+ kwargs[:bounds] << ba
413
+ else
414
+ raise "expecting interval vectors, not #{b.class}"
415
+ end
416
+ end
417
+ kwargs[:type] = 'boxes'
418
+ kwargs[:format] = color if color.is_a?(String)
419
+ draw(kwargs)
420
+ end
421
+
422
+
423
+ # Draw the union of multiple boxes.
424
+ # @param boxes [IntervalVector, Array<IntervalVector>] Boxes or an array of boxes
425
+ def draw_boxes_union(*boxes, color: nil, **kwargs)
426
+ kwargs[:bounds] = []
427
+ boxes.flatten!
428
+ color = boxes.select{|v| v.is_a?(String)}.first if color.nil?
429
+ boxes.reject!{|v| v.is_a?(String)}
430
+ boxes.each do |b|
431
+ if VIBes::object_is_a_box?(b) then
432
+ ba = []
433
+ b.each do |v|
434
+ ba << v.lower_bound
435
+ ba << v.upper_bound
436
+ end
437
+ kwargs[:bounds] << ba
438
+ else
439
+ raise "expecting interval vectors, not #{b.class}"
440
+ end
441
+ end
442
+ kwargs[:type] = 'boxes union'
443
+ kwargs[:format] = color if color.is_a?(String)
444
+ draw(kwargs)
445
+ end
446
+
447
+
448
+ # Draw a point.
449
+ # @overload draw_point(cx, cy, radius: 2, color: 'k', **kwargs)
450
+ # @param cx [Float] point _x_ position
451
+ # @param cy [Float] point _y_ position
452
+ # @param radius [Float] point display radius in pixel
453
+ def draw_point(*a, radius: 2, color: nil, **kwargs)
454
+ kwargs[:type] = :point
455
+ kwargs[:point] = a.flatten.collect(&:to_f)
456
+ kwargs[:Radius] = radius.to_f if radius
457
+ kwargs[:format] = color if color.is_a?(String)
458
+ draw(kwargs)
459
+ end
460
+
461
+
462
+ # Draw multiple points at once
463
+ # @example
464
+ # # Draw three points
465
+ # f.draw_points([-1, -1], [0, 1], [1, -1])
466
+ # @see draw_point
467
+ def draw_points(*a, radius: 2, color: nil, **kwargs)
468
+ el = nil
469
+ a.each do |e|
470
+ raise "expecting arrays of the same size" unless e.is_a?(Array) and (el.nil? or e.length == el)
471
+ el = e.length
472
+ end
473
+ kwargs[:type] = :points
474
+ kwargs[:centers] = a.collect{|e| e.collect(&:to_f)}
475
+ kwargs[:Radius] = radius.to_f if radius
476
+ kwargs[:format] = color if color.is_a?(String)
477
+ draw(kwargs)
478
+ end
479
+
480
+
481
+ # Draw a line between two points
482
+ # @overload draw_line(a_start, a_end, color: 'r', **kwargs)
483
+ # @param [Array<Float>] point coordinates in arrays
484
+ # @overload draw_line(p1: a_start, p2: a_end, color: 'k', **kwargs)
485
+ # @param [Array<Float>] point coordinates in arrays
486
+ # @example
487
+ # draw_line([0, 0], [1, 1])
488
+ # draw_line(p1: [0, 0], p2: [1, 1])
489
+ def draw_line(ps=nil, pe=nil, p1: [0, 0], p2: [1, 1], color: nil, **kwargs)
490
+ kwargs[:type] = :line
491
+ p1 = ps if ps.is_a?(Array)
492
+ p2 = pe if pe.is_a?(Array)
493
+ raise 'The start and end points must have the same number of coordinates' if p1.length != p2.length
494
+ kwargs[:points] = [p1.collect(&:to_f), p2.collect(&:to_f)]
495
+ kwargs[:format] = color if color.is_a?(String)
496
+ draw(kwargs)
497
+ end
498
+
499
+
500
+ # Draw an arrow
501
+ # @example
502
+ # # Draw an array from (0,0) to (1,1), with a tip length of 0.2
503
+ # f.draw_arrow([0, 0], [1, 1], 0.2, color: 'r[r]')
504
+ # f.draw_arrow(p1: [0, 0], p2: [1, 1], tip_length: 0.2, color: 'k[k]')
505
+ def draw_arrow(ps=nil, pe=nil, tl=nil, p1: [0, 0], p2: [1, 1], tip_length: nil, color: nil, **kwargs)
506
+ kwargs[:type] = :arrow
507
+ p1 = ps if ps.is_a?(Array)
508
+ p2 = pe if pe.is_a?(Array)
509
+ raise 'The start and end points must have the same number of coordinates' if p1.length != p2.length
510
+ kwargs[:points] = [p1.collect(&:to_f), p2.collect(&:to_f)]
511
+ kwargs[:format] = color if color.is_a?(String)
512
+ if tl != nil then
513
+ kwargs[:tip_length] = tl.to_f
514
+ elsif tip_length.kind_of?(Numeric) then
515
+ kwargs[:tip_length] = tip_length.to_f
516
+ elsif p1.length == 2 and p2.length == 2 then
517
+ kwargs[:tip_length] = Math.sqrt((p2.first - p1.first)**2 + (p2.last - p1.last)**2)/10.0
518
+ else
519
+ kwargs[:tip_length] = 0.1
520
+ end
521
+ draw(kwargs)
522
+ end
523
+
524
+
525
+ # Draw a circle.
526
+ # @example
527
+ # # Draw a circle of center (-1,0) and radius 0.5
528
+ # f.draw_circle([-1, 0], 0.5)
529
+ def draw_circle(c, r, color: nil, **kwargs)
530
+ draw_ellipse(c, [r, r], color: color, **kwargs)
531
+ end
532
+
533
+
534
+ # Draw a Ring.
535
+ # @example
536
+ # # Draw a ring of center (-1,0), min radius: 0.5 and max radius: 1
537
+ # f.draw_ring([-1, 0], [0.5, 1], color: 'r[g]')
538
+ # f.draw_ring(center: [-1, 0], radius: [0.5, 1], color: 'r[g]')
539
+ def draw_ring(cent=nil, rad=nil, center: [0, 0], radius: [1, 2], color: nil, **kwargs)
540
+ kwargs[:type] = :ring
541
+ kwargs[:center] = (cent.is_a?(Array) ? cent : center).collect(&:to_f)
542
+ kwargs[:rho] = (rad.is_a?(Array) ? rad : radius).collect(&:to_f)
543
+ kwargs[:format] = color if color.is_a?(String)
544
+ draw(kwargs)
545
+ end
546
+
547
+
548
+ # Draw a polygon
549
+ # @example
550
+ # f.draw_polygon([-1, -1], [0, 1], [1, -1])
551
+ def draw_polygon(*a, color: nil, **kwargs)
552
+ el = nil
553
+ a.each do |e|
554
+ raise "expecting arrays of the same size" unless e.is_a?(Array) and (e.length == el or el.nil?)
555
+ el = e.length
556
+ end
557
+ kwargs[:type] = :polygon
558
+ kwargs[:bounds] = a.collect{|e| e.collect(&:to_f)}
559
+ kwargs[:format] = color if color.is_a?(String)
560
+ draw(kwargs)
561
+ end
562
+
563
+
564
+ # Draw an image.
565
+ # @param fname [String] file name of the image to draw
566
+ # @param pos [Array<Float>] (x,y) position of the upper left corner of the image on the figure
567
+ # @param resolution [Array<Float>] horizontal and vertical resolution to display the image. If the vertical resolution is positive, the image may be displayed upside-down.
568
+ # @example
569
+ # # Display lena.png (512x512 pixels) so tha it fills the box (0,1)x(0,1)
570
+ # f.draw_raster('lena.png', pos: [0, 1], resolution: [1.0/512, -1.0/512])
571
+ # f.draw_raster('lena.png', [0, 1], [1.0/512, -1.0/512])
572
+ def draw_raster(fname, po=nil, re=nil, pos: [0, 0], resolution: [1, -1], **kwargs)
573
+ kwargs[:type] = :raster
574
+ kwargs[:filename] = fname.to_s
575
+ kwargs[:ul_corner] = (po.is_a?(Array) ? po : pos).collect(&:to_f)
576
+ kwargs[:scale] = (re.is_a?(Array) ? re : resolution).collect(&:to_f)
577
+ draw(kwargs)
578
+ end
579
+
580
+
581
+ # Draw a pie
582
+ # @param center [Array<Float>]
583
+ # @param radius [Array<Float>] minimal and maximal radius
584
+ # @param theta [Array<Float>] minimal and maximal angle, in radians or degrees depending on _use_radians_
585
+ # @param use_radians [Boolean] use radians or degrees
586
+ # @example
587
+ # f.draw_pie(center: [0, 0], radius: [1, 2], theta: [0, 45], use_radians: false)
588
+ # f.draw_pie([0, 0], [1, 2], [0, 45], color: '[r]')
589
+ def draw_pie(cent=nil, rad=nil, thet=nil, center: [0, 0], radius: [1, 2], theta: [0, 45], use_radians: false, color: nil, **kwargs)
590
+ kwargs[:type] = :pie
591
+ kwargs[:center] = (cent.is_a?(Array) ? cent : center).collect(&:to_f)
592
+ kwargs[:rho] = (rad.is_a?(Array) ? rad : radius).collect(&:to_f)
593
+ kwargs[:theta] = (thet.is_a?(Array) ? thet : theta).collect(&:to_f)
594
+ kwargs[:theta].collect!{|v| v*180/Math::PI} if use_radians
595
+ kwargs[:format] = color if color.is_a?(String)
596
+ draw(kwargs)
597
+ end
598
+
599
+
600
+ # Draw text on the figure
601
+ # @note Does not seem to work on Linux
602
+ def draw_text(str=nil, po=nil, s=nil, text: '', pos: [0, 0], scale: 1, color: nil, **kwargs)
603
+ kwargs[:type] = :text
604
+ kwargs[:text] = (str.nil? ? text : str).to_s
605
+ kwargs[:position] = (po.is_a?(Array) ? po : pos).collect(&:to_f)
606
+ kwargs[:scale] = (s.nil? ? scale : s).to_f
607
+ kwargs[:format] = color if color.is_a?(String)
608
+ draw(kwargs)
609
+ end
610
+
611
+
612
+ # Draw an AUV (yellow submarine) centered at _center_, with heading _heading_ degrees and size _length_.
613
+ # @param center [Array<Float>] Center of the AUV
614
+ # @param heading [Float] heading of the AUV in degree
615
+ # @param length [Float] size of the AUV
616
+ # @example
617
+ # f.draw_AUV(center: [0, -1], heading: 45, length: 1.0, color: 'k[y]')
618
+ # f.draw_AUV([0, -1], 45, 1.0, color: 'k[y]')
619
+ def draw_AUV(cent=nil, rt=nil, len=nil, center: [0, 0], heading: 0.0, length: 1.0, color: nil, **kwargs)
620
+ kwargs[:type] = :vehicle_auv
621
+ kwargs[:center] = (cent.is_a?(Array) ? cent : center).collect(&:to_f)
622
+ kwargs[:orientation] = (rt.nil? ? heading : rt).to_f
623
+ kwargs[:length] = Math.sqrt((len.nil? ? length : len).to_f)
624
+ kwargs[:format] = color if color.is_a?(String)
625
+ draw(kwargs)
626
+ end
627
+
628
+
629
+ # Draw a vehicle centered at _center_, with heading _heading_ degrees and of size _length_.
630
+ # @example
631
+ # f.draw_vehicle(center: [0, -1], heading: 45, length: 1.0, color: 'k[y]')
632
+ # f.draw_vehicle([0, -1], 45, 1.0, color: 'k[y]')
633
+ def draw_vehicle(cent=nil, rt=nil, len=nil, center: [0, 0], heading: 0.0, length: 1.0, color: nil, **kwargs)
634
+ kwargs[:type] = :vehicle
635
+ kwargs[:center] = (cent.is_a?(Array) ? cent : center).collect(&:to_f)
636
+ kwargs[:orientation] = (rt.nil? ? heading : rt).to_f
637
+ kwargs[:length] = Math.sqrt((len.nil? ? length : len).to_f)
638
+ kwargs[:format] = color if color.is_a?(String)
639
+ draw(kwargs)
640
+ end
641
+
642
+
643
+ # Draw a tank centered at _center_, with heading _heading_ degrees and of size _length_.
644
+ # @example
645
+ # f.draw_tank(center: [0, -1], heading: 45, length: 1.0, color: 'k[g]')
646
+ # f.draw_tank([0, -1], 45, 1.0, color: 'k[g]')
647
+ def draw_tank(cent=nil, rt=nil, len=nil, center: [0, 0], heading: 0.0, length: 1.0, color: nil, **kwargs)
648
+ kwargs[:type] = :vehicle_tank
649
+ kwargs[:center] = (cent.is_a?(Array) ? cent : center).collect(&:to_f)
650
+ kwargs[:orientation] = (rt.nil? ? heading : rt).to_f
651
+ kwargs[:length] = (len.nil? ? length : len).to_f
652
+ kwargs[:format] = color if color.is_a?(String)
653
+ draw(kwargs)
654
+ end
655
+
656
+
657
+ # Draw an ellipse centered at (cx,cy) with semi-major and minor axes _a_ and _b_, and rotated by _rot_ degrees.
658
+ def draw_ellipse(cent=nil, ax=nil, rt=nil, center: nil, cx: 0.0, cy: 0.0, axis: nil, a: 1.0, b: a, rot: 0.0, color: nil, **kwargs)
659
+ kwargs = {} if kwargs.nil?
660
+ kwargs[:type] = :ellipse
661
+ if cent.is_a?(Array) then
662
+ kwargs[:center] = cent.collect(&:to_f)
663
+ else
664
+ kwargs[:center] = center.is_a?(Array) ? center.collect(&:to_f) : [cx.to_f, cy.to_f]
665
+ end
666
+ if ax.is_a?(Array) then
667
+ kwargs[:axis] = ax.collect(&:to_f)
668
+ else
669
+ kwargs[:axis] = axis.is_a?(Array) ? axis.collect(&:to_f) : [a.to_f, b.to_f]
670
+ end
671
+ kwargs[:orientation] = rt.nil? ? rot.to_f : rt.to_f
672
+ kwargs[:format] = color if color.is_a?(String)
673
+ draw(kwargs)
674
+ end
675
+
676
+
677
+ def draw_confidence_ellipse(cent=nil, cov=nil, sig=nil, center: nil, cx: 0.0, cy: 0.0, covariance: nil, sxx: 1.0, sxy: 1.0, syy: 1.0, sigma: 1.0, color: nil, **kwargs)
678
+ kwargs = {} if kwargs.nil?
679
+ kwargs[:type] = :ellipse
680
+ if cent.is_a?(Array) then
681
+ kwargs[:center] = cent.collect(&:to_f)
682
+ else
683
+ kwargs[:center] = center.is_a?(Array) ? center.collect(&:to_f) : [cx.to_f, cy.to_f]
684
+ end
685
+ if cov.is_a?(Array) then
686
+ kwargs[:covariance] = cov.collect(&:to_f)
687
+ else
688
+ kwargs[:covariance] = covariance.is_a?(Array) ? covariance.collect(&:to_f) : [sxx.to_f, sxy.to_f, sxy.to_f, syy.to_f]
689
+ end
690
+ kwargs[:sigma] = sig.nil? ? sigma.to_f : sig.to_f
691
+ kwargs[:format] = color if color.is_a?(String)
692
+ draw(kwargs)
693
+ end
694
+
695
+
696
+ # @!endgroup
697
+
698
+
699
+ def remove_object(object_name, **kwargs)
700
+ kwargs[:action] = :delete
701
+ kwargs[:figure] = @name
702
+ kwargs[:object] = object_name.to_s
703
+ VIBes.msg "#{kwargs.to_json}\n\n"
704
+ self
705
+ end
706
+
707
+
708
+ def set_object_properties(object_name, **kwargs)
709
+ h = {action: :set, figure: @name, object: object_name.to_s, properties: kwargs.to_json}
710
+ VIBes.msg "#{h.to_json}\n\n"
711
+ self
712
+ end
713
+
714
+
715
+ private
716
+
717
+
718
+ def draw(kwargs)
719
+ VIBes.msg "{\"action\":\"draw\",\"figure\":\"#{@name}\",\"shape\":#{kwargs.to_json}}\n\n"
720
+ self
721
+ end
722
+ end
723
+
724
+
725
+ # Check if an object is an interval vector,
726
+ # using @@interval_vector_class_cache
727
+ # @!visibility private
728
+ def self.object_is_a_box?(obj)
729
+ return true if @@interval_vector_class_cache[obj.class] == true
730
+ if obj.respond_to?(:is_an_interval_vector?) and obj.is_an_interval_vector? == true then
731
+ @@interval_vector_class_cache[obj.class] = true
732
+ return true
733
+ end
734
+ return false
735
+ end
736
+
737
+
738
+ # Check if an object is an interval,
739
+ # using @@interval_class_cache
740
+ # @!visibility private
741
+ def self.object_is_an_interval?(obj)
742
+ return true if @@interval_class_cache[obj.class] == true
743
+ if obj.respond_to?(:is_an_interval?) and obj.is_an_interval? == true then
744
+ @@interval_class_cache[obj.class] = true
745
+ return true
746
+ end
747
+ return false
748
+ end
749
+
750
+
751
+ # @see object_is_a_box?
752
+ # @!visibility private
753
+ @@interval_vector_class_cache = {}
754
+
755
+ # @see object_is_an_interval?
756
+ # @!visibility private
757
+ @@interval_class_cache = {}
758
+ end
759
+
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require '../lib/vibes-rb.rb'
4
+
5
+ f = VIBes::Figure.new
6
+ f.width = 512
7
+ f.height = 512
8
+ f.axis_limits(-1.2 .. 1.2, -1.2 .. 1.2)
9
+
10
+ t = 0.0
11
+ loop do
12
+ x = Math.cos(t)
13
+ y = Math.sin(2*t)
14
+ dx = -Math.sin(t)
15
+ dy = 2*Math.cos(2*t)
16
+ a = 180*Math.atan2(dy, dx)/Math::PI
17
+ t += 0.01
18
+ f.draw_point(x, y)
19
+ f.remove_object('car')
20
+ f.draw_vehicle([x, y], a, 0.2, color: 'k[y]', name: 'car')
21
+ break if t >= 2*Math::PI
22
+ t -= 2*Math::PI if t >= 2*Math::PI
23
+ sleep(0.02)
24
+ end
25
+
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'vibes-rb'
4
+
5
+ f = VIBes::Figure.new 'all figures'
6
+
7
+ f.x = 100
8
+ f.y = 100
9
+ f.width = 500
10
+ f.height = 500
11
+
12
+ f.draw_arrow([-1, -1], [1, 1], 0.6, color: 'k[k]')
13
+ f.draw_AUV([4, 0], 45, 2, color: 'k[y]')
14
+ f.draw_box([7, 9], [-2, 2], color: '[b]')
15
+ f.draw_tank([12, 0], 45, 3, color: 'k[r]')
16
+ f.draw_box([7, 9], [-2, 2], color: '[b]')
17
+ f.draw_circle([0, -4], 1, color: 'g[m]')
18
+ f.draw_ellipse([4, -4], [2,1], 45, color: 'r[darkCyan]')
19
+ f.draw_line([7, -5], [9, -3], color: 'k')
20
+ f.draw_pie([10, -6], [1, 2.5], [20,70], color: 'y[cyan]')
21
+ f.draw_polygon([-1, -9], [1, -9], [0, -7], color: 'k[orange]')
22
+ f.draw_ring([4, -8], [1, 2], color: '[red]')
23
+ f.draw_vehicle([8, -8], 20, 1, color: '[darkBlue]')
24
+ f.draw_point(12, -8, radius: 2, color: '[k]')
25
+
26
+ f.axis_equal
27
+
data/vibes-rb.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ require File.expand_path('../lib/vibes-rb/version.rb', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'vibes-rb'
5
+ s.version = VIBes::VERSION
6
+ s.date = Time.now.strftime '%Y-%m-%d'
7
+ s.summary = "Ruby API to connect to VIBes-viewer (Visualizer for Intervals and Boxes)."
8
+ s.description = "The vibes-rb gem allows to connect to the VIBes viewer (Visualizer for Intervals and Boxes). See https://github.com/ENSTABretagneRobotics/VIBES"
9
+ s.license = 'LGPL-3.0'
10
+ s.authors = ["Théotime Bollengier"]
11
+ s.email = 'theotime.bollengier@ensta-bretagne.fr'
12
+ s.homepage = 'https://gitlab.ensta-bretagne.fr/bollenth/vibes.rb'
13
+ s.add_runtime_dependency 'json', '~> 2.3', '>= 2.3.0'
14
+ s.files = [
15
+ 'LICENSE',
16
+ 'README.md',
17
+ 'vibes-rb.gemspec',
18
+ '.yardopts',
19
+ 'lib/vibes-rb.rb',
20
+ 'lib/vibes-rb/version.rb',
21
+ 'samples/example_allfigures.rb',
22
+ 'samples/animation.rb'
23
+ ]
24
+ end
25
+
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vibes-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.3
5
+ platform: ruby
6
+ authors:
7
+ - Théotime Bollengier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.3.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.3'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.3.0
33
+ description: The vibes-rb gem allows to connect to the VIBes viewer (Visualizer for
34
+ Intervals and Boxes). See https://github.com/ENSTABretagneRobotics/VIBES
35
+ email: theotime.bollengier@ensta-bretagne.fr
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - ".yardopts"
41
+ - LICENSE
42
+ - README.md
43
+ - lib/vibes-rb.rb
44
+ - lib/vibes-rb/version.rb
45
+ - samples/animation.rb
46
+ - samples/example_allfigures.rb
47
+ - vibes-rb.gemspec
48
+ homepage: https://gitlab.ensta-bretagne.fr/bollenth/vibes.rb
49
+ licenses:
50
+ - LGPL-3.0
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.2.3
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Ruby API to connect to VIBes-viewer (Visualizer for Intervals and Boxes).
71
+ test_files: []