oozby 0.3.1 → 0.4.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: 65d655bb878ca1d6f884b0ed18f710669e64d6f7
4
- data.tar.gz: 4d9e1326e16f2436caa73b9a1a73a43dca9e7a18
3
+ metadata.gz: 582eb27392751e57f1f1f4b40721f90d1c00b119
4
+ data.tar.gz: 475814b52c59cfe2914341010f8fd52c8b1329cc
5
5
  SHA512:
6
- metadata.gz: 87f4f684352daf605291502a0c470c38a181e2263dac1588f177c818486db5a5092e6abd3e6190ac10bbb67e451ac3bb39769e98a88961ea986d868f6c6de151
7
- data.tar.gz: 244a08c28e31d3cfd781fd895d34df5c843b2f1f15aa68e1f89ab7173809653b2adf15b1bf407bd4d86255dd622d3b7a97cfb008c93cb7259353a05edca67bfa
6
+ metadata.gz: 65b282a8ea18ee15d80c339bb389bcd03220b8afbe85c3e30ca3f538d6c730b58dabd69a10a987eb2142c5ff8fa31d0788247c1507eb935a821441c8a1e86464
7
+ data.tar.gz: 56048c0db5681f7e0e6164f3bea334e967bb61651d506b175622a27402fbb34067ea3ea8c66c387f0d5444d722699caf328ff251a3d34243e2f6568178311c7e
data/bin/oozby CHANGED
@@ -1,20 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
- require 'thor'
3
- require 'listen'
4
- require 'pp'
5
- require 'rbconfig'
2
+ require 'thor' # command line utility utility
3
+ require 'listen' # listen to filesystem for changes in a smart way
4
+ require 'pp' # pretty printer
5
+ require 'rbconfig' # to get path to ruby binary, for making subprocesses
6
+ require 'colored' # ANSI colouring
6
7
  require File.join(__dir__, '..', 'lib', 'oozby')
7
8
 
8
- # motivational exit messages!
9
- $quit_message = {
10
- '0.3.0' => "Have a nice day!",
11
- '0.3.1' => "You're a superstar!",
12
- '0.3.2' => "Keep up the great work!",
13
- '0.3.3' => "Nice job!",
14
- '0.3.4' => "It was a pleasure working with you!"
15
- }
16
-
17
9
  class OozbyUtility < Thor
10
+ QuitMessage = "All done! ^_^"
18
11
  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."
19
12
 
20
13
  option :verbose, type: :boolean, desc: "Output lots of gunk, to figure out bizarre bugs in Oozby"
@@ -39,10 +32,11 @@ class OozbyUtility < Thor
39
32
  handle.write ooz.render
40
33
  end
41
34
 
42
- puts "compiled #{File.basename(path)}"
35
+ print "[#{Time.now.strftime "%H:%M:%S"}] "
36
+ puts "Compiled #{File.basename(path).underline}".green
43
37
  rescue StandardError, ScriptError, NoMethodError => err
44
38
  local_pwd = Dir.pwd
45
- puts "#{err.class.name}: #{err.message.sub(local_pwd + File::SEPARATOR, '')}"
39
+ puts "#{err.class.name.reversed}: #{err.message.sub(local_pwd + File::SEPARATOR, '').red}"
46
40
  err.backtrace.each { |line| puts line.sub(local_pwd + File::SEPARATOR, '') }
47
41
  puts nil, nil
48
42
  end
@@ -60,7 +54,7 @@ class OozbyUtility < Thor
60
54
  subprocess_compile File.join(*args)
61
55
  end
62
56
 
63
- puts "Watching folder for changes... (CTRL+C to exit)"
57
+ puts "Watching folder for changes...".blue + " (" + "CTRL+C".bold + " to exit)"
64
58
  Listen.to! directory, filter: /\.oozby$/ do |modified, added, removed|
65
59
  modified.each { |path| recompile_handler[path] }
66
60
  added.each { |path| recompile_handler[path] }
@@ -68,7 +62,7 @@ class OozbyUtility < Thor
68
62
  puts "#{File.basename(path)} deleted."
69
63
  if File.exists? "#{path}.scad"
70
64
  File.delete("#{path}.scad")
71
- puts "Deleted generated .scad file for obsolete #{File.basename(path)}"
65
+ puts "Deleted #{(File.basename(path) + '.scad').underline}".green
72
66
  end
73
67
  end
74
68
  end
@@ -76,7 +70,7 @@ class OozbyUtility < Thor
76
70
  end
77
71
  rescue Interrupt => e
78
72
  puts ""
79
- puts $quit_message[Oozby.version] || $quit_message.values.last
73
+ puts QuitMessage.blue
80
74
  end
81
75
 
82
76
  private
@@ -1,7 +1,8 @@
1
1
  warn "Requires ruby 2.0 or newer" unless RUBY_VERSION.split('.').first.to_i >= 2
2
2
  require_relative 'oozby/base'
3
3
  require_relative 'oozby/environment'
4
- require_relative 'oozby/method_preprocessor'
4
+ require_relative 'oozby/preprocessor'
5
+ require_relative 'oozby/preprocessor-definitions'
5
6
  require_relative 'oozby/render'
6
7
  require_relative 'oozby/element'
7
8
  require_relative 'oozby/version'
@@ -2,42 +2,43 @@
2
2
  class Oozby::Element
3
3
  attr_reader :data, :siblings
4
4
 
5
- def initialize hash, parent_array
5
+ def initialize hash
6
6
  @data = hash
7
- @siblings = parent_array
7
+ @siblings = nil #parent_array
8
8
  end
9
9
 
10
10
  # include the next thing as a child of this thing
11
11
  def > other
12
- if other.respond_to? :to_oozby_element
13
- other = [other]
14
- elsif not other.is_a?(Array) or other.any? { |item| not item.respond_to? :to_oozby_element }
15
- raise "Can only use > combiner to add Oozby::Element or array of Elements"
16
- end
17
-
18
- # convert all items to real Oozby Elements
19
- other = other.map { |item| item.to_oozby_element }
20
-
21
- # add others as children
22
- children.push *(other.map { |item| item.data })
23
-
24
- # remove children from their previous parents
25
- other.each do |thing|
26
- thing.siblings = children
27
- end
28
-
29
- other.first
12
+ raise "Can only combine an Oozby::Element to another Oozby::Element" unless other.respond_to? :to_oozby_element
13
+ other.to_oozby_element.abduct self.children
14
+ other
30
15
  end
31
16
 
32
- def index
33
- @siblings.index(@data)
17
+ # add this element to the end of supplied array
18
+ def abduct into
19
+ @siblings.delete self if @siblings # remove from current location
20
+ @siblings = into # our new location is here
21
+ into.push self # append ourselves to the new parent
34
22
  end
35
23
 
36
- def siblings= new_parent
37
- @siblings.delete @data
38
- @siblings = new_parent
24
+ def index
25
+ @siblings.index(self)
39
26
  end
40
27
 
28
+ # def siblings= new_parent
29
+ # @siblings.delete self
30
+ # @siblings = new_parent
31
+ # end
32
+
33
+ # replace this element with any number of other elements and hashes
34
+ # def replace *others
35
+ # raise "Can't replace Oozby::Element with things that aren't hash-like" unless others.all? { |x| x.respond_to? :to_h }
36
+ # puts "Replacing: #{self} with [#{others.join('; ')}]"
37
+ # idx = self.index
38
+ # others.each { |x| x.siblings = @siblings if x.respond_to? :siblings= }
39
+ # @siblings[idx..idx] = others
40
+ # end
41
+
41
42
  [:children, :modifier, :method, :args, :named_args].each do |hash_accessor_name|
42
43
  define_method(hash_accessor_name) { @data[hash_accessor_name] }
43
44
  define_method("#{hash_accessor_name}=") { |val| @data[hash_accessor_name] = val }
@@ -51,6 +52,16 @@ class Oozby::Element
51
52
  end
52
53
  end
53
54
 
55
+ def to_s
56
+ x = args.map { |x| x.inspect }
57
+ named_args.each { |name, value| x.push "#{name}: #{value.inspect}"}
58
+ "#{method}(#{x.join(', ')})"
59
+ end
60
+
61
+ def to_h
62
+ @data
63
+ end
64
+
54
65
  def to_oozby_element
55
66
  self
56
67
  end
@@ -20,7 +20,7 @@ class Oozby::Environment
20
20
  @modifier = nil
21
21
  @one_time_modifier = nil
22
22
  @preprocess = true
23
- @method_preprocessor = Oozby::MethodPreprocessor.new(env: self, ooz: @parent)
23
+ @method_preprocessor = Oozby::Preprocessor.new(env: self, ooz: @parent)
24
24
  @scanned_scad_files = []
25
25
  end
26
26
 
@@ -80,19 +80,17 @@ class Oozby::Environment
80
80
  children = []
81
81
  end
82
82
 
83
- call = {
83
+ element = Oozby::Element.new({
84
84
  method: method_name,
85
85
  args: args, named_args: hash,
86
86
  children: children,
87
87
  modifier: @one_time_modifier || @modifier,
88
88
  call_address: @ast.length
89
- }
89
+ })
90
90
 
91
- @ast.push(comment: "oozby code: " + JSON.generate(call)) if @parent.debug
92
-
93
- @ast.push call
94
- element = Oozby::Element.new(call, @ast)
95
- @method_preprocessor.transform_call(element) if @preprocess
91
+ @ast.push(comment: "oozby: #{element}") if @parent.debug
92
+ element = @method_preprocessor.transform_call(element) if @preprocess
93
+ element.abduct @ast
96
94
  @one_time_modifier = nil
97
95
 
98
96
  return element
@@ -0,0 +1,403 @@
1
+ class Oozby::Preprocessor
2
+ ##############################################################################
3
+ ########## All PUBLIC methods below this line are Preprocessors! #############
4
+ ##############################################################################
5
+ public
6
+
7
+ default_filters [:xyz, default: 0]
8
+ passthrough :rotate
9
+ passthrough :translate
10
+ passthrough :mirror
11
+ default_filters [:xyz, default: 1]
12
+ passthrough :scale
13
+ passthrough :resize
14
+
15
+ default_filters # none for these guys
16
+ passthrough :multmatrix
17
+ passthrough :color
18
+
19
+ default_filters :resolution, :layout_defaults, :expanded_names
20
+
21
+ # detect requests for rounded cubes and transfer them over
22
+ filter :xyz, depth: true, default: 1 # cube has xy coords
23
+ filter :rename_args, [:r, :cr, :corner_r] => :corner_radius
24
+ filter :validate, size: [Array, Numeric], center: [true, false], corner_radius: Numeric
25
+ def cube size: [1,1,1], center: false, corner_radius: 0
26
+ return rounded_rectangular_prism(size: size, center: center, corner_radius: corner_radius) if corner_radius > 0
27
+ return call
28
+ end
29
+
30
+
31
+ # detect requests for rounded cylinders and transfer them over
32
+ filter :rename_args, [:cr, :corner_r] => :corner_radius
33
+ filter :validate, h: Numeric, r1: [Numeric, nil], r2: [Numeric, nil], r: [Numeric, nil], center: [true, false], corner_radius: Numeric
34
+ def cylinder h: 1, r1: nil, r2: nil, r: nil, center: false, corner_radius: 0
35
+ r1, r2 = r, r if r unless r1 || r2
36
+ return rounded_cylinder(h: h, r1: r1, r2: r2, center: center, corner_radius: corner_radius) if corner_radius > 0
37
+ return call
38
+ end
39
+
40
+ passthrough :sphere
41
+ #passthrough :cylinder
42
+ passthrough :polyhedron
43
+ # 2d shapes
44
+
45
+ # detect requests for rounded squares and transfer them over
46
+ filter :xyz, arg: :size, depth: false, default: 1 # square has xy coords
47
+ filter :rename_args, [:r, :cr, :corner_r] => :corner_radius
48
+ filter :validate, size: [Array, Numeric], center: [true, false], corner_radius: Numeric
49
+ def square size: [1,1], center: false, corner_radius: 0
50
+ return rounded_rectangle(size: size, center: center, corner_radius: corner_radius) if corner_radius > 0
51
+ return call
52
+ end
53
+
54
+ passthrough :circle
55
+ passthrough :polygon
56
+ # extrude 2d shapes to 3d shapes
57
+ filter :expanded_names, height_label: :height
58
+ passthrough :linear_extrude
59
+ passthrough :rotate_extrude
60
+
61
+ default_filters # none
62
+ passthrough :minkowski
63
+ passthrough :hull
64
+ passthrough :import
65
+ passthrough :projection
66
+
67
+ passthrough :union
68
+ passthrough :difference
69
+ passthrough :intersection
70
+ passthrough :render
71
+
72
+ ### Various ngons and prisms:
73
+ # http://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons
74
+ polygon_names = {
75
+ triangle: 3,
76
+ equilateral_triangle: 3,
77
+ pentagon: 5,
78
+ hexagon: 6,
79
+ heptagon: 7,
80
+ octagon: 8,
81
+ nonagon: 9,
82
+ enneagon: 9,
83
+ decagon: 10,
84
+ hendecagon: 11,
85
+ undecagon: 11,
86
+ dodecagon: 12,
87
+ tridecagon: 13,
88
+ tetradecagon: 14,
89
+ pentadecagon: 15,
90
+ hexadecagon: 16,
91
+ heptadecagon: 17,
92
+ octadecagon: 18,
93
+ enneadecagon: 19,
94
+ icosagon: 20,
95
+ triacontagon: 30,
96
+ tetracontagon: 40,
97
+ pentacontagon: 50,
98
+ hexacontagon: 60,
99
+ heptacontagon: 70,
100
+ octacontagon: 80,
101
+ enneacontagon: 90,
102
+ hectogon: 100
103
+ }
104
+ polygon_names.each do |shape_name, sides|
105
+ oozby_alias shape_name, :circle, sides: sides
106
+ end
107
+
108
+ # make a polygon with an arbitrary number of sides
109
+ oozby_alias :ngon, :circle, sides: 3
110
+ # make a prism with an arbitrary number of sides
111
+ oozby_alias :prism, :cylinder, sides: 3
112
+
113
+ # triangles are an edge case
114
+ oozby_alias :triangular_prism, :cylinder, sides: 3
115
+
116
+ # for all the rest, transform the prism names automatically
117
+ polygon_names.each do |poly_name, sides|
118
+ name = poly_name.to_s
119
+ if name.end_with? 'gon'
120
+ name += 'al_prism'
121
+ oozby_alias name, :cylinder, sides: sides
122
+ end
123
+ end
124
+
125
+
126
+
127
+ ##############################################################################
128
+ ######################### Begin Filters section! #############################
129
+ ##############################################################################
130
+ private
131
+ # apply resolution settings to element
132
+ ResolutionNames = { degrees_per_fragment: "$fa", minimum: "$fs", fragments: "$fn" }
133
+ def resolution
134
+ res = @env.resolution
135
+ res.delete_if { |k,v| Oozby::Environment::ResolutionDefaults[k] == v }
136
+ res.each do |key, value|
137
+ call.named_args[(ResolutionNames[key] || "$#{key}").to_sym] ||= value
138
+ end
139
+ end
140
+
141
+ # filter to patch in layout defaults like center: true when not specified
142
+ # explicitly in this call
143
+ def layout_defaults
144
+ # copy in defaults if not already specified
145
+ call.named_args.merge!(@env.defaults) { |k,a,b| a }
146
+ end
147
+
148
+ # filter to rename certain arguments to other things
149
+ # Usage> filter :rename_args, :old_arg => :new_arg, :other => morer
150
+ def rename_args pairs
151
+ pairs.each do |from_keys, to_key|
152
+ from_keys = [from_keys] unless from_keys.is_a? Array
153
+ if from_keys.any? { |key| call.named_args[key.to_sym] }
154
+ value = from_keys.map { |key| call.named_args.delete(key) }.compact.first
155
+ call.named_args[to_key.to_sym] = value if value != nil
156
+ end
157
+ end
158
+ end
159
+
160
+ # general processing of arguments:
161
+ # -o> Friendly names - use radius instead of r if you like
162
+ # -o> Ranges - can specify radius: 5...10 instead of r1: 5, r2: 10
163
+ # -o> Make h/height consistent (either works everywhere)
164
+ # -o> Support inner radius, when number of sides is specified
165
+ # -o> Specify diameter and have it halved automatically
166
+ def expanded_names height_label: :h
167
+ # let users use 'radius' as longhand for 'r', and some other stuff
168
+ rename_args(
169
+ [:radius] => :r,
170
+ [:radius1, :radius_1] => :r1,
171
+ [:radius2, :radius_2] => :r2,
172
+ [:facets, :fragments, :sides] => :"$fn",
173
+ [:inr, :inradius, :in_radius, :inner_r, :inner_radius] => :ir,
174
+ [:height, :h] => height_label
175
+ )
176
+
177
+ # let users specify diameter instead of radius - convert it
178
+ { diameter: :r, dia: :r, d: :r,
179
+ diameter1: :r1, diameter_1: :r1, dia1: :r1, dia_1: :r1, d1: :r1,
180
+ diameter2: :r2, diameter_2: :r2, dia2: :r2, dia_2: :r2, d2: :r2,
181
+ id: :ir, inner_diameter: :ir, inner_d: :ir,
182
+ id1: :ir1, inner_diameter_1: :ir1, inner_diameter1: :ir1,
183
+ id2: :ir2, inner_diameter_2: :ir2, inner_diameter2: :ir2
184
+ }.each do |d, r|
185
+ if call.named_args.key? d
186
+ data = call.named_args.delete(d)
187
+ if data.is_a? Range
188
+ data = Range.new(data.first / 2.0, data.last / 2.0, data.exclude_end?)
189
+ elsif data.respond_to? :to_f
190
+ data = data.to_f / 2.0
191
+ else
192
+ raise "#{data.inspect} must be Numeric or a Range"
193
+ end
194
+ call.named_args[r] = data
195
+ end
196
+ end
197
+
198
+ # process 'inner radius' bits
199
+ { ir: :r, ir1: :r1, ir2: :r2 }.each do |ir, r|
200
+ if call.named_args.key? ir
201
+ sides = call.named_args[:"$fn"]
202
+ raise "Use of inner_radius requires sides/facets/fragments argument to #{call.method}()" unless sides.is_a? Numeric
203
+ raise "sides/facets/fragments argument must be a whole number (Fixnum)" unless sides.is_a? Fixnum
204
+ raise "sides/facets/fragments argument must be at least 3 #{call} to use inner_radius" unless sides >= 3
205
+ inradius = call.named_args.delete(ir)
206
+ if inradius.is_a? Range
207
+ circumradius = Range.new(inradius.first.to_f / @env.cos(180.0 / sides),
208
+ inradius.first.to_f / @env.cos(180.0 / sides),
209
+ inradius.exclude_end?)
210
+ elsif inradius.respond_to? :to_f
211
+ circumradius = inradius.to_f / @env.cos(180.0 / sides)
212
+ else
213
+ raise "#{inradius.inspect} must be Numeric or a Range"
214
+ end
215
+ call.named_args[r] = circumradius
216
+ end
217
+ end
218
+
219
+ # convert range radius to r1 and r2 pair
220
+ if call.named_args[:r].is_a? Range
221
+ range = call.named_args.delete(:r)
222
+ call.named_args[:r1] = range.first
223
+ call.named_args[:r2] = range.last
224
+ end
225
+ end
226
+
227
+ # filter calls to a method, transforming x, y, and optionally z arguments in
228
+ # to a 2 or 3 item array, setting it to the argument named 'arg' or setting
229
+ # it as the first numerically indexed argument if that is unspecified. A
230
+ # default value can be supplied.
231
+ # Usage> filter :xyz, default: 1, arg: :size, depth: false
232
+ def xyz default: 0, arg: false, depth: true
233
+ if [:x, :y, :z].any? { |name| call.named_args.include? name }
234
+ # validate args
235
+ [:x, :y, :z].each do |key|
236
+ if call.named_args.has_key? key
237
+ unless call.named_args[key].is_a? Numeric
238
+ raise "#{key} must be Numeric, value #{call.named_args[key].inspect} is not."
239
+ end
240
+ end
241
+ end
242
+
243
+ coords = [call.named_args.delete(:x), call.named_args.delete(:y)]
244
+ coords.push call.named_args.delete(:z) if depth
245
+ coords.map! { |x| x or default } # apply default value to missing data
246
+
247
+ # if argument name is specified, use that, otherwise make it the first argument in the call
248
+ if arg
249
+ call.named_args[arg] = coords
250
+ else
251
+ call.args.unshift coords
252
+ end
253
+ end
254
+ end
255
+
256
+
257
+ # simple validator to check particular named arguments conform to required types
258
+ # or exactly match a set of values
259
+ # Usage> filter :validate, argument_name: Symbol, other_argument: ["yes", "no"], radius: [Numeric, Range]
260
+ def validate args = {}
261
+ args.keys.each do |args_keys|
262
+ acceptable = if args[args_keys].respond_to? :each then args[args_keys] else [args[args_keys]] end
263
+ key_list = if args_keys.respond_to? :each then args_keys else [args_keys] end
264
+ key_list.each do |key|
265
+ # for this key, check it matches acceptable list, if specified
266
+ if call.named_args.keys.include? key
267
+ value = call.named_args[key]
268
+ if acceptable.none? { |accepts| accepts === value }
269
+ raise "#{call.method}'s argument #{key} must be #{acceptable.inspect}"
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ # require certain arguments be specified to a processed method
277
+ # Usage> filter :require_args, :first_arg, :second_arg
278
+ def require_args *list
279
+ list.each do |name|
280
+ raise "#{call.method} requires argument #{name}" unless call.named_args.keys.include? name
281
+ end
282
+ end
283
+
284
+
285
+ def rounded_rectangle size: [1,1], center: false, corner_radius: 0.0, facets: nil
286
+ size = [size] * 2 if size.is_a? Numeric
287
+ size = [size[0] || 1, size[1] || 1]
288
+ raise "Corner radius is too big. Max #{size.min / 2.0} for this square" if corner_radius * 2.0 > size.min
289
+ corner_diameter = corner_radius * 2.0
290
+ circle_x = (size[0] / 2.0) - corner_radius
291
+ circle_y = (size[1] / 2.0) - corner_radius
292
+
293
+ capture do
294
+ resolution(fragments: (facets || 0)) do
295
+ translate(if center then [0,0] else [size[0] / 2.0, size[1] / 2.0] end) do
296
+ union do
297
+ square([size[0], size[1] - corner_diameter], center = true)
298
+ square([size[0] - corner_diameter, size[1]], center = true)
299
+ preprocessor true do
300
+ resolution(fragments: (_fragments_for(radius: corner_radius).to_f / 4.0).round * 4.0) do
301
+ translate([ circle_x, circle_y]) { circle(r: corner_radius) }
302
+ translate([ circle_x, -circle_y]) { circle(r: corner_radius) }
303
+ translate([-circle_x, -circle_y]) { circle(r: corner_radius) }
304
+ translate([-circle_x, circle_y]) { circle(r: corner_radius) }
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
312
+
313
+ # create a rounded cylinder shape
314
+ def rounded_cylinder h: 1, r1: 1, r2: 1, center: false, corner_radius: 0
315
+ radii = [r1, r2]
316
+ raise "corner_radius is too big. Max is #{radii.min} for this cylinder" if corner_radius > radii.min
317
+ corner_diameter = corner_radius * 2
318
+
319
+ preprocessor = self
320
+ # use rounded rect to create the body shape
321
+ capture do
322
+ facets = preprocessor.call.named_args[:"$fn"] || _fragments_for(radius: radii.min)
323
+
324
+ translate([0,0, if center then -h / 2.0 else 0 end]) >
325
+ #union do
326
+ rotate_extrude(:"$fn" => facets) do
327
+ hull do
328
+ # table to calculate radii at in between y positions
329
+ table = { 0.0 => r1, h.to_f => r2 }
330
+ # offset taking in to account angle of wall, as the line between each
331
+ # circle after the hull operation will not be from exactly corner_radius
332
+ # height when the side angle is not 90deg
333
+ lookup_offset = corner_radius * sin(atan2(r2-r1, h) / 2.0)
334
+ # bottom right corner
335
+ translate([lookup(corner_radius + lookup_offset, table) - corner_radius, h - corner_radius]) >
336
+ circle(r: corner_radius, :"$fn" => facets)
337
+ # top right corner
338
+ translate([lookup(h - corner_radius + lookup_offset, table) - corner_radius, corner_radius]) >
339
+ circle(r: corner_radius, :"$fn" => facets)
340
+ # center point
341
+ square([radii.min - corner_radius, h])
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ # handle rounded cubes
348
+ def rounded_rectangular_prism size: [1,1,1], center: false, corner_radius: 0, facets: nil
349
+ size = [size] * 3 if size.is_a? Numeric
350
+ size = [size[0] || 1, size[1] || 1, size[2] || 1]
351
+ raise "Radius is too big. Max #{size.min / 2.0} for this cube" if corner_radius * 2.0 > size.min
352
+ corner_diameter = corner_radius.to_f * 2.0
353
+
354
+ preprocessor = self
355
+ # use rounded rect to create the body shape
356
+ capture do
357
+ resolution(fragments: (facets || 0)) do
358
+ union do
359
+ offset = if center then [0,0,0] else [size[0].to_f / 2.0, size[1].to_f / 2.0, size[2].to_f / 2.0] end
360
+ translate(offset) do
361
+ # extrude the main body parts using rounded_rectangle as the basis
362
+ linear_extrude(height: size[2] - corner_diameter, center: true) {
363
+ inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[0], size[1]], center: true, corner_radius: corner_radius)) }
364
+ rotate([90,0,0]) { linear_extrude(height: size[1] - corner_diameter, center: true) {
365
+ inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[0], size[2]], center: true, corner_radius: corner_radius)) }}
366
+ rotate([0,90,0]) { linear_extrude(height: size[0] - corner_diameter, center: true) {
367
+ inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[2], size[1]], center: true, corner_radius: corner_radius)) }}
368
+
369
+ # fill in the corners with spheres
370
+ xr, yr, zr = size.map { |x| (x / 2.0) - corner_radius }
371
+ corner_coordinates = [
372
+ [ xr, yr, zr],
373
+ [ xr, yr,-zr],
374
+ [ xr,-yr, zr],
375
+ [ xr,-yr,-zr],
376
+ [-xr, yr, zr],
377
+ [-xr, yr,-zr],
378
+ [-xr,-yr, zr],
379
+ [-xr,-yr,-zr]
380
+ ]
381
+ preprocessor true do
382
+ resolution(fragments: (_fragments_for(radius: corner_radius.to_f).to_f / 4.0).round * 4.0) do
383
+ corner_coordinates.each do |coordinate|
384
+ translate(coordinate) do
385
+ # generate sphere shape
386
+ rotate_extrude do
387
+ intersection do
388
+ circle(r: corner_radius)
389
+ translate([corner_radius, 0, 0]) { square([corner_radius * 2.0, corner_radius * 4.0], center: true) }
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
402
+
403
+
@@ -0,0 +1,183 @@
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
+ class Oozby::Preprocessor
5
+ @@method_filters ||= {}
6
+ @@default_filters ||= []
7
+ @@queued_filters ||= []
8
+ # metaprogramming for hooking up filters before method definitions
9
+ class << self
10
+ # sets a list of default filters which aren't reset after each method def
11
+ def default_filters *list
12
+ @@default_filters = list.map { |x| if x.is_a? Array then x else [x] end }
13
+ @@queued_filters = @@default_filters.dup
14
+ end
15
+
16
+ # set a filter to be added only to the next method def
17
+ def filter filter_name, *options
18
+ @@queued_filters ||= @@default_filters.dup
19
+ @@queued_filters.delete_if { |x| x[0] == filter_name } # replace default definitions
20
+ @@queued_filters.push([filter_name, *options]) # add new filter definition
21
+ end
22
+
23
+ # detect a method def, store it's filters and reset for next def
24
+ def method_added method_name
25
+ finalize_filter method_name
26
+ super
27
+ end
28
+
29
+ # def passthrough method_name
30
+ # @@method_filters[method_name] = @@queued_filters
31
+ # @@queued_filters = @@default_filters.dup
32
+ # end
33
+
34
+ def finalize_filter method_name
35
+ @@method_filters[method_name] = @@queued_filters
36
+ @@queued_filters = @@default_filters.dup
37
+ end
38
+
39
+ # don't want to define a primary processor method? pass it through manually
40
+ def passthrough method_name, *arg_names
41
+ arg_list = arg_names.map { |x| "#{x}: nil" }.join(', ')
42
+ define_method method_name, &eval("->(#{arg_list}) {nil}")
43
+ end
44
+
45
+ # get list of filters for a method name
46
+ def filters_for method_name
47
+ @@method_filters[method_name] || []
48
+ end
49
+
50
+ # alias an name to an openscad method optionally with extra defaults
51
+ # useful for giving things more descriptive names, where those names imply
52
+ # different defaults, like hexagon -> circle(sides: 6)
53
+ def oozby_alias from, to, **extra_args
54
+ define_method(from) do
55
+ call.named_args.merge!(extra_args) #{ |key,l,r| l } # left op wins conflicts
56
+ run_filters to
57
+ redirect to
58
+ end
59
+ end
60
+ end
61
+
62
+ # never pass resolution data in to these methods - it's pointless:
63
+ NoResolution = %i(translate rotate scale mirror resize difference union
64
+ intersection hull minkowski color)
65
+ # list of OpenSCAD standard methods - these can pass through without error:
66
+ DefaultOpenSCADMethods = %i(
67
+ cube sphere cylinder polyhedron
68
+ circle square polygon
69
+ scale resize rotate translate mirror multmatrix color minkowski hull
70
+ linear_extrude rotate_extrude import import_dxf projection
71
+ union difference intersection render
72
+ )
73
+
74
+ attr_accessor :openscad_methods, :call
75
+ # setup a new method preprocessor
76
+ def initialize env: nil, ooz: nil
77
+ @env = env
78
+ @parent = ooz
79
+ @openscad_methods = DefaultOpenSCADMethods.dup
80
+ end
81
+
82
+ # accepts an Oozby::Element and transforms it according to the processors' rules
83
+ def transform_call call_info
84
+ raise "call info isn't Oozby::Element #{call_info.inspect}" unless call_info.is_a? Oozby::Element
85
+ @call = call_info
86
+ original_method = @call.method
87
+
88
+ run_filters call_info.method.to_sym
89
+
90
+ methods = primary_processors
91
+ # if a primary processor is defined for this kind of call
92
+ if methods.include? call_info.method.to_sym
93
+ # call the primary processor
94
+ result = public_send(call_info.method, *primary_method_args)
95
+
96
+ # replace the ast content with the processor's output
97
+ if result.is_a? Hash or result.is_a? Oozby::Element
98
+ # replace called item with this new stuff
99
+ return result
100
+ elsif result != nil # ignore nil - we don't need to do anything for that!
101
+ raise "#{original_method} preprocessor returned invalid result #{result.inspect}"
102
+ end
103
+ end
104
+
105
+ return call_info
106
+ end
107
+
108
+ def run_filters method_name
109
+ # apply the other filters
110
+ filters = self.class.filters_for(method_name)
111
+ filters.each do |filter_data|
112
+ filter_name, *filter_args = filter_data
113
+ send(filter_name, *filter_args)
114
+ end
115
+ end
116
+
117
+ # generates argument list to call a primary method processor
118
+ def primary_method_args # :nodoc:
119
+ # grab the primary processor
120
+ primary = self.class.public_instance_method(call.method.to_sym)
121
+ params = primary.parameters
122
+ # parse the processor method's signature
123
+ param_names = params.select { |x| x.first == :key }.map { |x| x[1] }
124
+ # filter down args to only those requested
125
+ calling_args = call.named_args.dup
126
+ # convert unnamed call args to named args
127
+ calling_args.merge! args_parse(call, *param_names)
128
+ # delete any args the receiver can't handle
129
+ calling_args.delete_if { |k,v| not param_names.include?(k) }
130
+ if calling_args.empty? then [] else [calling_args] end
131
+ end
132
+
133
+ # list of primary processor methods
134
+ def primary_processors
135
+ @primary_processors ||= public_methods(false) - @@system_methods
136
+ end
137
+
138
+ # does this processor know of a method named whatever?
139
+ def known? name
140
+ known.include? name.to_sym
141
+ end
142
+
143
+ # array of all known method names
144
+ def known
145
+ list = @openscad_methods.dup
146
+ list.push *primary_processors
147
+ list.push *@@method_filters.keys
148
+ list.uniq
149
+ end
150
+
151
+ # rewrite this method to a different method name and primary processor and whatever else
152
+ def redirect new_method
153
+ call.method = new_method.to_sym
154
+ public_send(new_method, *primary_method_args) if self.respond_to? new_method
155
+ end
156
+
157
+
158
+ # parse arguments like openscad does
159
+ def args_parse(info, *arg_names)
160
+ args = info.named_args.dup
161
+ info.args.length.times do |index|
162
+ warn "Overwriting argument #{arg_names[index]}" if args.key? arg_names[index]
163
+ args[arg_names[index]] = info.args[index]
164
+ end
165
+
166
+ args
167
+ end
168
+
169
+ # capture contents of a block as openscad code, returning AST array
170
+ def capture &proc
171
+ env = @env
172
+ (env._subscope {
173
+ env.preprocessor(false) {
174
+ env._execute_oozby(&proc)
175
+ }
176
+ }).find { |x| x.is_a? Oozby::Element }
177
+ end
178
+
179
+ # remember list of public methods defined so far - these are system ones
180
+ @@system_methods = public_instance_methods(false)
181
+ end
182
+
183
+
@@ -1,5 +1,5 @@
1
1
  class Oozby
2
- Version = '0.3.1'
2
+ Version = '0.4.0'
3
3
 
4
4
  def self.version
5
5
  Oozby::Version
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.3.1
4
+ version: 0.4.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-29 00:00:00.000000000 Z
11
+ date: 2013-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: listen
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.2.11
55
+ - !ruby/object:Gem::Dependency
56
+ name: colored
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
55
69
  description: OpenSCAD - a cad language for creating solid 3d objects, useful for CNC
56
70
  and 3D Printing, is incredibly annoying. It doesn't even support variables! Oozby
57
71
  is a markup builder like Markaby or XML Builder, so you can write OpenSCAD programs
@@ -67,7 +81,8 @@ files:
67
81
  - lib/oozby/base.rb
68
82
  - lib/oozby/element.rb
69
83
  - lib/oozby/environment.rb
70
- - lib/oozby/method_preprocessor.rb
84
+ - lib/oozby/preprocessor-definitions.rb
85
+ - lib/oozby/preprocessor.rb
71
86
  - lib/oozby/render.rb
72
87
  - lib/oozby/version.rb
73
88
  - lib/oozby.rb
@@ -1,362 +0,0 @@
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
-
6
- class Oozby::MethodPreprocessor
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
- )
17
-
18
- attr_accessor :openscad_methods
19
- # setup a new method preprocessor
20
- def initialize env: nil, ooz: nil
21
- @env = env
22
- @parent = ooz
23
- @openscad_methods = DefaultOpenSCADMethods.dup
24
- end
25
-
26
- # accepts an Oozby::Element and transforms it according to the processors' rules
27
- def transform_call(call_info)
28
- send("_#{call_info.method}", call_info) if respond_to? "_#{call_info.method}"
29
- resolution call_info unless NoResolution.include? call_info.method # apply resolution settings from scope
30
- return call_info
31
- end
32
-
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
47
- def xyz_to_array(info, default: 0, arg: false, depth: true)
48
- if [:x, :y, :z].any? { |name| info[:named_args].include? name }
49
-
50
- # create coordinate 'vector' (array)
51
- if depth
52
- coords = [
53
- info[:named_args][:x] || default,
54
- info[:named_args][:y] || default,
55
- info[:named_args][:z] || default
56
- ]
57
- else
58
- coords = [
59
- info[:named_args][:x] || default,
60
- info[:named_args][:y] || default
61
- ]
62
- end
63
-
64
- # if argument name is specified, use that, otherwise make it the first argument in the call
65
- if arg
66
- info[:named_args][arg] = coords
67
- else
68
- info[:args].unshift coords
69
- end
70
-
71
- # delete unneeded bits from call
72
- [:x, :y, :z].each { |item| info[:named_args].delete item }
73
- end
74
- end
75
-
76
- # layout defaults like {center: true}
77
- def layout_defaults(info)
78
- info[:named_args] = @env.defaults.dup.merge(info[:named_args])
79
- end
80
-
81
- # apply resolution settings to element
82
- ResolutionLookupTable = {degrees_per_fragment: :"$fa", minimum: :"$fs", fragments: :"$fn"}
83
- def resolution(info)
84
- res = @env.resolution
85
- res.delete_if { |k,v| Oozby::Environment::ResolutionDefaults[k] == v }
86
- res.each do |key, value|
87
- info[:named_args][ResolutionLookupTable[key] || "$#{key}".to_sym] ||= value
88
- end
89
- end
90
-
91
- def _translate(info); xyz_to_array(info, arg: :v); end
92
- def _rotate(info); xyz_to_array(info, arg: :a); end
93
- def _scale(info); xyz_to_array(info, default: 1, arg: :v); end
94
- def _mirror(info); xyz_to_array(info); end
95
- def _resize(info); xyz_to_array(info, default: 1, arg: :newsize); end
96
- def _cube(info)
97
- xyz_to_array(info, default: 1, arg: :size)
98
- expanded_names(info)
99
- layout_defaults(info)
100
-
101
- if info.named_args[:r] && info.named_args[:r] > 0.0
102
- # render rounded rectangle
103
- info.replace rounded_cube(**args_parse(info, :size, :center))
104
- end
105
- end
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
113
- def expanded_names(info, height_label: :h)
114
- # let users use 'radius' as longhand for 'r'
115
- info.named_args[:r] = info.named_args.delete(:radius) if info.named_args[:radius]
116
- info.named_args[:r1] = info.named_args.delete(:radius1) if info.named_args[:radius1]
117
- info.named_args[:r1] = info.named_args.delete(:radius_1) if info.named_args[:radius_1]
118
- info.named_args[:r2] = info.named_args.delete(:radius2) if info.named_args[:radius2]
119
- info.named_args[:r2] = info.named_args.delete(:radius_2) if info.named_args[:radius_2]
120
-
121
- info.named_args[:"$fn"] = info.named_args.delete(:facets) if info.named_args[:facets]
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]
129
-
130
- # let users specify diameter instead of radius - convert it
131
- { diameter: :r, dia: :r, d: :r,
132
- diameter1: :r1, diameter_1: :r1, dia1: :r1, dia_1: :r1, d1: :r1,
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
137
- }.each do |d, r|
138
- if info.named_args.key? d
139
- data = info.named_args.delete(d)
140
- if data.is_a? Range
141
- data = Range.new(data.first / 2.0, data.last / 2.0, data.exclude_end?)
142
- elsif data.respond_to? :to_f
143
- data = data.to_f / 2.0
144
- else
145
- raise "#{data.inspect} must be Numeric or a Range"
146
- end
147
- info.named_args[r] = data
148
- end
149
- end
150
-
151
- # process 'inner radius' bits
152
- { ir: :r, ir1: :r1, ir2: :r2 }.each do |ir, r|
153
- if info.named_args.key? ir
154
- sides = info.named_args[:"$fn"].to_i
155
- raise "Use of inner radius requires sides/facets/fragments argument to #{info.method}()" unless sides
156
- raise "Sides must be at least 3" unless sides >= 3
157
- inradius = info.named_args.delete(ir)
158
- if inradius.is_a? Range
159
- circumradius = Range.new(inradius.first.to_f / @env.cos(180.0 / sides),
160
- inradius.first.to_f / @env.cos(180.0 / sides),
161
- inradius.exclude_end?)
162
- elsif inradius.respond_to? :to_f
163
- circumradius = inradius.to_f / @env.cos(180.0 / sides)
164
- else
165
- raise "#{inradius.inspect} must be Numeric or a Range"
166
- end
167
- info.named_args[r] = circumradius
168
- end
169
- end
170
-
171
- # allow range for radius
172
- if info.named_args[:r].is_a? Range
173
- range = info.named_args.delete(:r)
174
- info.named_args[:r1] = range.first
175
- info.named_args[:r2] = range.last
176
- end
177
-
178
- # long version 'height' becomes 'h'
179
- height_specification = info.named_args.delete(:height) || info.named_args.delete(:h)
180
- info.named_args[height_label] = height_specification if height_specification
181
- end
182
-
183
- def _linear_extrude(info); layout_defaults(info); expanded_names(info, height_label: :height); end
184
- def _rotate_extrude(info); layout_defaults(info); expanded_names(info); end
185
-
186
- def _circle(info); expanded_names(info); end
187
- def _sphere(info); expanded_names(info); end
188
- def _cylinder(info); expanded_names(info); layout_defaults(info); end
189
- def _square(info)
190
- expanded_names(info)
191
- xyz_to_array(info, default: 1, arg: :size, depth: false)
192
- layout_defaults(info)
193
-
194
- if info[:named_args][:r] and info.named_args[:r] > 0.0
195
- # render rounded rectangle
196
- info.replace rounded_rect(**args_parse(info, :size, :center).merge(facets: info.named_args["$fn"]))
197
- end
198
- end
199
-
200
- def rounded_rect size: [1,1], center: false, r: 0.0, facets: nil
201
- size = [size] * 2 if size.is_a? Numeric
202
- diameter = r * 2
203
- circle_x = (size[0] / 2.0) - r
204
- circle_y = (size[1] / 2.0) - r
205
-
206
- capture do
207
- resolution(fragments: (facets || 0)) do
208
- translate(if center then [0,0] else [size[0].to_f / 2.0, size[1].to_f / 2.0] end) do
209
- union do
210
- square([size[0], size[1] - diameter], center = true)
211
- square([size[0] - diameter, size[1]], center = true)
212
- preprocessor true do
213
- resolution(fragments: (_fragments_for(radius: r).to_f / 4.0).round * 4.0) do
214
- translate([ circle_x, circle_y]) { circle(r: r) }
215
- translate([ circle_x, -circle_y]) { circle(r: r) }
216
- translate([-circle_x, -circle_y]) { circle(r: r) }
217
- translate([-circle_x, circle_y]) { circle(r: r) }
218
- end
219
- end
220
- end
221
- end
222
- end
223
- end
224
- end
225
-
226
- def rounded_cube size: [1,1,1], center: false, r: 0.0, facets: nil
227
- size = [size] * 3 if size.is_a? Numeric
228
- size = [size[0] || 1, size[1] || 1, size[2] || 1]
229
- diameter = r.to_f * 2.0
230
-
231
- preprocessor = self
232
- # use rounded rect to create the body shape
233
- capture do
234
- resolution(fragments: (facets || 0)) do
235
- offset = if center then [0,0,0] else [size[0].to_f / 2.0, size[1].to_f / 2.0, size[2].to_f / 2.0] end
236
- translate(offset) do
237
- # extrude the main body parts using rounded_rect as the basis
238
- linear_extrude(height: size[2] - diameter, center: true) {
239
- inject_abstract_tree(preprocessor.rounded_rect(size: [size[0], size[1]], center: true, r: r)) }
240
- rotate([90,0,0]) { linear_extrude(height: size[1] - diameter, center: true) {
241
- inject_abstract_tree(preprocessor.rounded_rect(size: [size[0], size[2]], center: true, r: r)) }}
242
- rotate([0,90,0]) { linear_extrude(height: size[0] - diameter, center: true) {
243
- inject_abstract_tree(preprocessor.rounded_rect(size: [size[2], size[1]], center: true, r: r)) }}
244
-
245
- # fill in the corners with spheres
246
- xr, yr, zr = size.map { |x| (x / 2.0) - r }
247
- corner_coordinates = [
248
- [ xr, yr, zr],
249
- [ xr, yr,-zr],
250
- [ xr,-yr, zr],
251
- [ xr,-yr,-zr],
252
- [-xr, yr, zr],
253
- [-xr, yr,-zr],
254
- [-xr,-yr, zr],
255
- [-xr,-yr,-zr]
256
- ]
257
- preprocessor true do
258
- resolution(fragments: (_fragments_for(radius: r.to_f).to_f / 4.0).round * 4.0) do
259
- corner_coordinates.each do |coordinate|
260
- translate(coordinate) do
261
- # generate sphere shape
262
- rotate_extrude do
263
- intersection do
264
- circle(r: r)
265
- translate([r, 0, 0]) { square([r * 2.0, r * 4.0], center: true) }
266
- end
267
- end
268
- end
269
- end
270
- end
271
- end
272
- end
273
- end
274
- end
275
- end
276
-
277
- # parse arguments like openscad does
278
- def args_parse(info, *arg_names)
279
- args = info[:named_args].dup
280
- info[:args].length.times do |index|
281
- warn "Overwriting argument #{arg_names[index]}" if args.key? arg_names[index]
282
- args[arg_names[index]] = info[:args][index]
283
- end
284
-
285
- args
286
- end
287
-
288
- def capture &proc
289
- env = @env
290
- (env._subscope {
291
- env.preprocessor(false) {
292
- env._execute_oozby(&proc)
293
- }
294
- }).first
295
- end
296
-
297
- # meta! construct aliases which preset some values
298
- def self.oozby_alias from, to, extra_args = {}
299
- define_method "_#{from}" do |info|
300
- info.method = to
301
- info.named_args.merge! extra_args
302
- send("_#{to}", info) if self.respond_to? "_#{to}"
303
- end
304
- end
305
-
306
- # some regular shapes - from:
307
- # http://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons
308
- polygon_names = {
309
- triangle: 3,
310
- equilateral_triangle: 3,
311
- pentagon: 5,
312
- hexagon: 6,
313
- heptagon: 7,
314
- octagon: 8,
315
- nonagon: 9,
316
- enneagon: 9,
317
- decagon: 10,
318
- hendecagon: 11,
319
- undecagon: 11,
320
- dodecagon: 12,
321
- tridecagon: 13,
322
- tetradecagon: 14,
323
- pentadecagon: 15,
324
- hexadecagon: 16,
325
- heptadecagon: 17,
326
- octadecagon: 18,
327
- enneadecagon: 19,
328
- icosagon: 20,
329
- triacontagon: 30,
330
- tetracontagon: 40,
331
- pentacontagon: 50,
332
- hexacontagon: 60,
333
- heptacontagon: 70,
334
- octacontagon: 80,
335
- enneacontagon: 90,
336
- hectogon: 100
337
- }
338
- polygon_names.each do |shape_name, sides|
339
- oozby_alias shape_name, :circle, sides: sides
340
- end
341
-
342
- # make a polygon with an arbitrary number of sides
343
- oozby_alias :ngon, :circle, sides: 3
344
- # make a prism with an arbitrary number of sides
345
- oozby_alias :prism, :cylinder, sides: 3
346
-
347
- # triangles are an edge case
348
- oozby_alias :triangular_prism, :cylinder, sides: 3
349
-
350
- # for all the rest, transform the prism names automatically
351
- polygon_names.each do |poly_name, sides|
352
- name = poly_name.to_s
353
- if name.end_with? 'gon'
354
- name += 'al_prism'
355
- oozby_alias name, :cylinder, sides: sides
356
- end
357
- end
358
- end
359
-
360
-
361
-
362
-