oozby 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a23a382871587b9a004386c00f558f07dbe317af
4
- data.tar.gz: b287f6a6be40890def253994c9413f5ba3140bad
3
+ metadata.gz: 5011b1b0b04f3a797f17d23572edc2bcefab387c
4
+ data.tar.gz: 20e1ce49a8c42a847bb40930d4c76b041674e85b
5
5
  SHA512:
6
- metadata.gz: e202ef9d0b2a7d76bf7cf885df73911e8f86f8240c6705760cccd86f34811db7ad80ae3c543b5e02145180b7fcfc888e6b3c0102cfcfd49e6f42435a312e9748
7
- data.tar.gz: 52a53ae63fe96411d3b1783db58466db03e83f3d5b17c6f5819d94ec7c356e4ca0e2afc49f07a3dc093f7f87c82d83958484d87658a90192cc27d8c127b95779
6
+ metadata.gz: ad55acc6736bbcd0c1fdc4c41b22daacbcae995dd9c915c7c95aac4a24038fbe66db4cfa622c0eacb3b8a133957a005a3642c2e5a8b56c3a5c6496c99f9b07fe
7
+ data.tar.gz: 1f47fca1a937a05c199686a62007b8f7246dde56112278fa9bfc34ee201eb165ade3da96e092795d62a4dd5fdf7caa68d3b465d1ddd20df68f9e12bc094b20ef
data/bin/oozby CHANGED
@@ -1,9 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
- $:.unshift File.dirname(__FILE__) + "/../lib"
3
2
  require 'thor'
4
3
  require 'listen'
5
4
  require 'pp'
6
- require 'oozby'
5
+ require File.join(__dir__, '..', 'lib', 'oozby')
6
+
7
+ # motivational exit messages!
8
+ $quit_message = {
9
+ '0.2.2' => "Have a nice day!",
10
+ '0.2.3' => "You're a superstar!",
11
+ '0.2.4' => "Keep up the great work!",
12
+ '0.2.5' => "Nice job!",
13
+ '0.2.6' => "It was a pleasure working with you!"
14
+ }
7
15
 
8
16
  class OozbyUtility < Thor
9
17
  desc "compile some#{File::SEPARATOR}folder", "The Oozby Utility searches a directory and compiles all the .oozby files it finds, creating identically named files with .scad stuck on the end, translated in to OpenSCAD nonsense. Open those files in OpenSCAD app and enable Automatic Reload and Compile in the Design menu, then get to work."
@@ -14,6 +22,9 @@ class OozbyUtility < Thor
14
22
  def compile path
15
23
  if File.file? path
16
24
  ooz = Oozby.new
25
+ ooz.filter_errors = !options[:verbose]
26
+ ooz.debug = options[:verbose]
27
+
17
28
  begin
18
29
  ooz.parse_file path
19
30
 
@@ -27,10 +38,11 @@ class OozbyUtility < Thor
27
38
  end
28
39
 
29
40
  puts "compiled #{File.basename(path)}"
30
- rescue StandardError, ScriptError => err
41
+ rescue StandardError, ScriptError, NoMethodError => err
31
42
  local_pwd = Dir.pwd
32
43
  puts "#{err.class.name}: #{err.message.sub(local_pwd + File::SEPARATOR, '')}"
33
44
  err.backtrace.each { |line| puts line.sub(local_pwd + File::SEPARATOR, '') }
45
+ puts nil, nil
34
46
  end
35
47
 
36
48
  elsif File.directory? path
@@ -53,7 +65,7 @@ class OozbyUtility < Thor
53
65
  removed.each do |path|
54
66
  puts "#{File.basename(path)} deleted."
55
67
  if File.exists? "#{path}.scad"
56
- File.delete("{path}.scad")
68
+ File.delete("#{path}.scad")
57
69
  puts "Deleted generated .scad file for obsolete #{File.basename(path)}"
58
70
  end
59
71
  end
@@ -62,7 +74,7 @@ class OozbyUtility < Thor
62
74
  end
63
75
  rescue Interrupt => e
64
76
  puts ""
65
- puts "Have a nice day!"
77
+ puts $quit_message[Oozby.version] || $quit_message.values.last
66
78
  end
67
79
  end
68
80
 
@@ -4,3 +4,4 @@ require_relative 'oozby/environment'
4
4
  require_relative 'oozby/method_preprocessor'
5
5
  require_relative 'oozby/render'
6
6
  require_relative 'oozby/element'
7
+ require_relative 'oozby/version'
@@ -2,25 +2,37 @@ require 'pp'
2
2
 
3
3
  # Oozby class loads up files and evaluates them to a syntax tree, and renders to openscad source code
4
4
  class Oozby
5
+ attr_accessor :filter_errors, :debug
6
+
5
7
  def initialize
6
8
  @code_tree = []
9
+ @filter_errors = false
10
+ @debug = false
7
11
  end
8
12
 
9
13
  # parse oozby code in to a syntax tree
10
14
  def parse code, filename: 'eval'
11
15
  env = Oozby::Environment.new(ooz: self)
16
+ if File.exists? filename
17
+ current_dir = Dir.pwd
18
+ Dir.chdir File.dirname(filename)
19
+ end
12
20
 
13
21
  # rescue block to filter out junk oozby library stuff from backtraces
14
22
  begin
15
23
  compiled = eval("lambda {; #{code};\n }", nil, filename)
16
24
  env.instance_exec(&compiled)
17
- rescue
18
- warn "Recent Calls: " + env.instance_variable_get(:@method_history).last(10).reverse.inspect
25
+ rescue StandardError, NoMethodError => err
19
26
  backtrace = $!.backtrace
20
- backtrace = backtrace.select { |item| !item.include? __dir__ } unless backtrace.first.include? __dir__
27
+ #backtrace = backtrace.select { |item| !item.include? __dir__ } unless backtrace.first.include? __dir__
28
+ backtrace.delete_if { |item| item.to_s =~ /(\.rb|\/oozby):/ } if @filter_errors
29
+
21
30
  raise $!, $!.message, backtrace
22
31
  end
23
32
  @code_tree = env._abstract_tree
33
+
34
+ ensure
35
+ Dir.chdir current_dir if current_dir
24
36
  end
25
37
 
26
38
  # parse a file containing oozby code in to a syntax tree
@@ -31,7 +43,7 @@ class Oozby
31
43
  # render the last parsed oozby code in to openscad source code
32
44
  def render
33
45
  renderer = Oozby::Render.new(ooz: self)
34
- renderer.render(@code_tree, clean: true).join("\n")
46
+ renderer.render(@code_tree, clean: true).join("\n") + "\n"
35
47
  end
36
48
 
37
49
  def abstract_tree
@@ -1,3 +1,5 @@
1
+ require 'amatch' # used to find possible intended method names
2
+
1
3
  class Oozby::Environment
2
4
  ResolutionDefaults = {
3
5
  degrees_per_fragment: 12,
@@ -14,8 +16,8 @@ class Oozby::Environment
14
16
  @modifier = nil
15
17
  @one_time_modifier = nil
16
18
  @preprocess = true
17
- @method_history = []
18
19
  @method_preprocessor = Oozby::MethodPreprocessor.new(env: self, ooz: @parent)
20
+ @scanned_scad_files = []
19
21
  end
20
22
 
21
23
  # create a new scope that inherits from this one, and capture syntax tree created
@@ -44,6 +46,21 @@ class Oozby::Environment
44
46
  end
45
47
 
46
48
  def method_missing method_name, *args, **hash, &proc
49
+ # unless we know of this method in OpenSCAD or the preprocessor, abort!
50
+ unless @method_preprocessor.known?(method_name) or !@preprocess
51
+ # grab a list of all known methods, suggest a guess to user
52
+ known = @method_preprocessor.known
53
+ known.push(*public_methods(false))
54
+ known.delete_if { |x| x.to_s.start_with? '_' }
55
+ matcher = Amatch::Sellers.new(method_name.to_s)
56
+ suggestion = known.min_by { |item| matcher.match(item.to_s) }
57
+
58
+ warn "Called unknown method #{method_name}()"
59
+ warn "Perhaps you meant #{suggestion}()?" if suggestion
60
+
61
+ return super # continue to raise the usual error and all that
62
+ end
63
+
47
64
  if proc
48
65
  children = _subscope(&proc)
49
66
  else
@@ -58,11 +75,13 @@ class Oozby::Environment
58
75
  call_address: @ast.length
59
76
  }
60
77
 
78
+ @ast.push(comment: "oozby code: " + JSON.generate(call)) if @parent.debug
79
+
61
80
  @ast.push call
62
81
  element = Oozby::Element.new(call, @ast)
63
82
  @method_preprocessor.transform_call(element) if @preprocess
64
83
  @one_time_modifier = nil
65
- @method_history.push method_name # maintain list of recent methods to aid debugging
84
+
66
85
  return element
67
86
  end
68
87
 
@@ -169,7 +188,44 @@ class Oozby::Environment
169
188
  _apply_modifier('!', &proc)
170
189
  end
171
190
 
191
+ def require *args
192
+ file = args.first
193
+ if file.end_with? '.scad'
194
+ _require_scad_file(*args)
195
+ elsif File.exists? "#{file}.scad"
196
+ _require_scad_file("#{args.shift}.scad", *args)
197
+ else
198
+ Kernel.require(*args)
199
+ end
200
+ end
201
+
202
+ def _require_scad_file filename, execute: true
203
+ raise "OpenSCAD file #{filename} not found" unless File.exists? filename
204
+ _scan_methods_from_scad_file filename
205
+
206
+ # add include statement to resulting openscad code
207
+ @ast.push(execute: !!execute, import: filename)
208
+ end
172
209
 
210
+ def _scan_methods_from_scad_file filename
211
+ raise "OpenSCAD file #{filename} not found" unless File.exists? filename
212
+ data = File.read(filename)
213
+ @scanned_scad_files.push filename
214
+
215
+ # parse out method definitions to add to our environment
216
+ data.gsub!(/\/\/.+?\n/m, "\n") # filter off single line comments
217
+ data.gsub!(/\/\*.+?\*\//m, '') # filter out multiline comments
218
+ data.scan /module[ \t]([a-zA-Z_9-9]+)/ do |module_name|
219
+ @method_preprocessor.openscad_methods.push module_name.first.to_sym
220
+ end
221
+
222
+ # find any references to more files and recurse in to those
223
+ data.scan /(use|include)[ \t]\<(.+?)\>/ do |filename|
224
+ unless @scanned_scad_files.include? filename
225
+ _scan_methods_from_scad_file filename
226
+ end
227
+ end
228
+ end
173
229
 
174
230
  def _apply_modifier new_modifier, &children
175
231
  if children
@@ -185,6 +241,8 @@ class Oozby::Environment
185
241
 
186
242
  # returns the abstract tree
187
243
  def _abstract_tree; @ast; end
244
+
245
+ def inspect; "OozbyFile"; end
188
246
  end
189
247
 
190
248
 
@@ -1,18 +1,49 @@
1
+ # The Oozby Method Preprocessor handles requests via the transform_call method
2
+ # and transforms the Oozby::Element passed in, patching in any extra features
3
+ # and trying to alert the user of obvious bugs
4
+
5
+
1
6
  class Oozby::MethodPreprocessor
2
- NoResolution = %i{translate rotate scale mirror resize difference union intersection hull minkowski}
7
+ # never pass resolution data in to these methods - it's pointless:
8
+ NoResolution = %i(translate rotate scale mirror resize difference union intersection hull minkowski color)
9
+ # list of OpenSCAD standard methods - these can pass through without error:
10
+ DefaultOpenSCADMethods = %i(
11
+ cube sphere cylinder polyhedron
12
+ circle square polygon
13
+ scale resize rotate translate mirror multmatrix color minkowski hull
14
+ linear_extrude rotate_extrude import projection
15
+ union difference intersection render
16
+ )
3
17
 
18
+ attr_accessor :openscad_methods
19
+ # setup a new method preprocessor
4
20
  def initialize env: nil, ooz: nil
5
21
  @env = env
6
22
  @parent = ooz
23
+ @openscad_methods = DefaultOpenSCADMethods.dup
7
24
  end
8
25
 
26
+ # accepts an Oozby::Element and transforms it according to the processors' rules
9
27
  def transform_call(call_info)
10
28
  send("_#{call_info.method}", call_info) if respond_to? "_#{call_info.method}"
11
29
  resolution call_info unless NoResolution.include? call_info.method # apply resolution settings from scope
12
30
  return call_info
13
31
  end
14
32
 
15
- # allow translate to take x, y, z named args instead of array, with defaults to 0
33
+ # does this processor know of a method named whatever?
34
+ def known? name
35
+ return true if respond_to? "_#{name}"
36
+ return @openscad_methods.include? name.to_sym
37
+ end
38
+
39
+ # array of all known method names
40
+ def known
41
+ list = @openscad_methods.dup
42
+ list.push *public_methods(false).select { |method_name| method_name.to_s.start_with? '_' }.map { |x| x.to_s.sub(/_/, '').to_sym }
43
+ list.uniq
44
+ end
45
+
46
+ # allow method to take x, y, z named args instead of array, with defaults to 0
16
47
  def xyz_to_array(info, default: 0, arg: false, depth: true)
17
48
  if [:x, :y, :z].any? { |name| info[:named_args].include? name }
18
49
 
@@ -42,12 +73,12 @@ class Oozby::MethodPreprocessor
42
73
  end
43
74
  end
44
75
 
45
- # layout defaults like center = whatever
76
+ # layout defaults like {center: true}
46
77
  def layout_defaults(info)
47
78
  info[:named_args] = @env.defaults.dup.merge(info[:named_args])
48
79
  end
49
80
 
50
- # apply resolution settings to call info
81
+ # apply resolution settings to element
51
82
  ResolutionLookupTable = {degrees_per_fragment: :"$fa", minimum: :"$fs", fragments: :"$fn"}
52
83
  def resolution(info)
53
84
  res = @env.resolution
@@ -73,6 +104,12 @@ class Oozby::MethodPreprocessor
73
104
  end
74
105
  end
75
106
 
107
+ # general processing of arguments:
108
+ # -o> Friendly names - use radius instead of r if you like
109
+ # -o> Ranges - can specify radius: 5...10 instead of r1: 5, r2: 10
110
+ # -o> Make h/height consistent (either works everywhere)
111
+ # -o> Support inner radius, when number of sides is specified
112
+ # -o> Specify diameter and have it halved automatically
76
113
  def expanded_names(info, height_label: :h)
77
114
  # let users use 'radius' as longhand for 'r'
78
115
  info.named_args[:r] = info.named_args.delete(:radius) if info.named_args[:radius]
@@ -83,11 +120,20 @@ class Oozby::MethodPreprocessor
83
120
 
84
121
  info.named_args[:"$fn"] = info.named_args.delete(:facets) if info.named_args[:facets]
85
122
  info.named_args[:"$fn"] = info.named_args.delete(:fragments) if info.named_args[:fragments]
123
+ info.named_args[:"$fn"] = info.named_args.delete(:sides) if info.named_args[:sides]
124
+
125
+ info.named_args[:ir] = info.named_args.delete(:inr) if info.named_args[:inr]
126
+ info.named_args[:ir] = info.named_args.delete(:inradius) if info.named_args[:inradius]
127
+ info.named_args[:ir] = info.named_args.delete(:inner_r) if info.named_args[:inner_r]
128
+ info.named_args[:ir] = info.named_args.delete(:inner_radius) if info.named_args[:inner_radius]
86
129
 
87
130
  # let users specify diameter instead of radius - convert it
88
131
  { diameter: :r, dia: :r, d: :r,
89
132
  diameter1: :r1, diameter_1: :r1, dia1: :r1, dia_1: :r1, d1: :r1,
90
- diameter2: :r2, diameter_2: :r2, dia2: :r2, dia_2: :r2, d2: :r2
133
+ diameter2: :r2, diameter_2: :r2, dia2: :r2, dia_2: :r2, d2: :r2,
134
+ id: :ir, inner_diameter: :ir, inner_d: :ir,
135
+ id1: :ir1, inner_diameter_1: :ir1, inner_diameter1: :ir1,
136
+ id2: :ir2, inner_diameter_2: :ir2, inner_diameter2: :ir2
91
137
  }.each do |d, r|
92
138
  if info.named_args.key? d
93
139
  data = info.named_args.delete(d)
@@ -100,6 +146,24 @@ class Oozby::MethodPreprocessor
100
146
  end
101
147
  end
102
148
 
149
+ # process 'inner radius' bits
150
+ { ir: :r, ir1: :r1, ir2: :r2 }.each do |ir, r|
151
+ if info.named_args.key? ir
152
+ sides = info.named_args[:"$fn"].to_i
153
+ raise "Use of inner radius requires sides/facets/fragments argument to #{info.method}()" unless sides
154
+ raise "Sides must be at least 3" unless sides >= 3
155
+ inradius = info.named_args.delete(ir)
156
+ if inradius.is_a? Range
157
+ circumradius = Range.new(inradius.first.to_f / @env.cos(180.0 / sides),
158
+ inradius.first.to_f / @env.cos(180.0 / sides),
159
+ inradius.exclude_end?)
160
+ else
161
+ circumradius = inradius.to_f / @env.cos(180.0 / sides)
162
+ end
163
+ info.named_args[r] = circumradius
164
+ end
165
+ end
166
+
103
167
  # allow range for radius
104
168
  if info.named_args[:r].is_a? Range
105
169
  range = info.named_args.delete(:r)
@@ -130,6 +194,7 @@ class Oozby::MethodPreprocessor
130
194
  end
131
195
 
132
196
  def rounded_rect size: [1,1], center: false, r: 0.0, facets: nil
197
+ size = [size] * 2 if size.is_a? Numeric
133
198
  diameter = r * 2
134
199
  circle_x = (size[0] / 2.0) - r
135
200
  circle_y = (size[1] / 2.0) - r
@@ -155,6 +220,7 @@ class Oozby::MethodPreprocessor
155
220
  end
156
221
 
157
222
  def rounded_cube size: [1,1,1], center: false, r: 0.0, facets: nil
223
+ size = [size] * 3 if size.is_a? Numeric
158
224
  size = [size[0] || 1, size[1] || 1, size[2] || 1]
159
225
  diameter = r.to_f * 2.0
160
226
 
@@ -173,7 +239,7 @@ class Oozby::MethodPreprocessor
173
239
  inject_abstract_tree(preprocessor.rounded_rect(size: [size[2], size[1]], center: true, r: r)) }}
174
240
 
175
241
  # fill in the corners with spheres
176
- xr, yr, zr = size.map { |x| (x / 2) - r }
242
+ xr, yr, zr = size.map { |x| (x / 2.0) - r }
177
243
  corner_coordinates = [
178
244
  [ xr, yr, zr],
179
245
  [ xr, yr,-zr],
@@ -185,14 +251,14 @@ class Oozby::MethodPreprocessor
185
251
  [-xr,-yr,-zr]
186
252
  ]
187
253
  preprocessor true do
188
- resolution(fragments: (_fragments_for(radius: r).to_f / 4.0).round * 4.0) do
254
+ resolution(fragments: (_fragments_for(radius: r.to_f).to_f / 4.0).round * 4.0) do
189
255
  corner_coordinates.each do |coordinate|
190
256
  translate(coordinate) do
191
257
  # generate sphere shape
192
258
  rotate_extrude do
193
259
  intersection do
194
260
  circle(r: r)
195
- translate([r, 0, 0]) { square([r * 2, r * 4], center: true) }
261
+ translate([r, 0, 0]) { square([r * 2.0, r * 4.0], center: true) }
196
262
  end
197
263
  end
198
264
  end
@@ -222,6 +288,68 @@ class Oozby::MethodPreprocessor
222
288
  }
223
289
  }).first
224
290
  end
291
+
292
+ # meta! construct aliases which preset some values
293
+ def self.oozby_alias from, to, extra_args = {}
294
+ define_method "_#{from}" do |info|
295
+ info.method = to
296
+ info.named_args.merge! extra_args
297
+ send("_#{to}", info) if self.respond_to? "_#{to}"
298
+ end
299
+ end
300
+
301
+ # some regular shapes - from:
302
+ # http://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons
303
+ polygon_names = {
304
+ triangle: 3,
305
+ equilateral_triangle: 3,
306
+ pentagon: 5,
307
+ hexagon: 6,
308
+ heptagon: 7,
309
+ octagon: 8,
310
+ nonagon: 9,
311
+ enneagon: 9,
312
+ decagon: 10,
313
+ hendecagon: 11,
314
+ undecagon: 11,
315
+ dodecagon: 12,
316
+ tridecagon: 13,
317
+ tetradecagon: 14,
318
+ pentadecagon: 15,
319
+ hexadecagon: 16,
320
+ heptadecagon: 17,
321
+ octadecagon: 18,
322
+ enneadecagon: 19,
323
+ icosagon: 20,
324
+ triacontagon: 30,
325
+ tetracontagon: 40,
326
+ pentacontagon: 50,
327
+ hexacontagon: 60,
328
+ heptacontagon: 70,
329
+ octacontagon: 80,
330
+ enneacontagon: 90,
331
+ hectogon: 100
332
+ }
333
+ polygon_names.each do |shape_name, sides|
334
+ oozby_alias shape_name, :circle, sides: sides
335
+ end
336
+
337
+ # make a polygon with an arbitrary number of sides
338
+ oozby_alias :ngon, :circle, sides: 3
339
+ # make a prism with an arbitrary number of sides
340
+ oozby_alias :prism, :cylinder, sides: 3
341
+
342
+ # triangles are an edge case
343
+ oozby_alias :triangular_prism, :cylinder, sides: 3
344
+
345
+ # for all the rest, transform the prism names automatically
346
+ polygon_names.each do |poly_name, sides|
347
+ name = poly_name.to_s
348
+ if name.end_with? 'gon'
349
+ name += 'al_prism'
350
+ oozby_alias name, :cylinder, sides: sides
351
+ end
352
+ end
225
353
  end
226
354
 
227
355
 
@@ -35,8 +35,12 @@ class Oozby::Render
35
35
  output.push "}"
36
36
  end
37
37
 
38
+ elsif node.key? :comment
39
+ output.push "/* #{node[:comment]} */"
38
40
  elsif node.key? :assign
39
- output << "#{node[:assign]} = #{escape(node[:value])};"
41
+ output.push "#{node[:assign]} = #{escape(node[:value])};"
42
+ elsif node.key? :import
43
+ output.push "#{node[:execute] ? 'include' : 'use'} <#{node[:import]}>;"
40
44
  end
41
45
  end
42
46
 
@@ -0,0 +1,7 @@
1
+ class Oozby
2
+ Version = '0.3.0'
3
+
4
+ def self.version
5
+ Oozby::Version
6
+ end
7
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oozby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bluebie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-06 00:00:00.000000000 Z
11
+ date: 2013-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: listen
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.18.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: amatch
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.11
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.11
41
55
  description: OpenSCAD - a cad language for creating solid 3d objects, useful for CNC
42
56
  and 3D Printing, is incredibly annoying. It doesn't even support variables! Oozby
43
57
  is a markup builder like Markaby or XML Builder, so you can write OpenSCAD programs
@@ -55,6 +69,7 @@ files:
55
69
  - lib/oozby/environment.rb
56
70
  - lib/oozby/method_preprocessor.rb
57
71
  - lib/oozby/render.rb
72
+ - lib/oozby/version.rb
58
73
  - lib/oozby.rb
59
74
  - readme.md
60
75
  - license.txt