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 +4 -4
- data/bin/oozby +12 -18
- data/lib/oozby.rb +2 -1
- data/lib/oozby/element.rb +36 -25
- data/lib/oozby/environment.rb +6 -8
- data/lib/oozby/preprocessor-definitions.rb +403 -0
- data/lib/oozby/preprocessor.rb +183 -0
- data/lib/oozby/version.rb +1 -1
- metadata +18 -3
- data/lib/oozby/method_preprocessor.rb +0 -362
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 582eb27392751e57f1f1f4b40721f90d1c00b119
|
4
|
+
data.tar.gz: 475814b52c59cfe2914341010f8fd52c8b1329cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
73
|
+
puts QuitMessage.blue
|
80
74
|
end
|
81
75
|
|
82
76
|
private
|
data/lib/oozby.rb
CHANGED
@@ -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/
|
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'
|
data/lib/oozby/element.rb
CHANGED
@@ -2,42 +2,43 @@
|
|
2
2
|
class Oozby::Element
|
3
3
|
attr_reader :data, :siblings
|
4
4
|
|
5
|
-
def initialize hash
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
33
|
-
|
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
|
37
|
-
@siblings.
|
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
|
data/lib/oozby/environment.rb
CHANGED
@@ -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::
|
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
|
-
|
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
|
92
|
-
|
93
|
-
@ast
|
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
|
+
|
data/lib/oozby/version.rb
CHANGED
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.
|
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-
|
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/
|
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
|
-
|