openscad-text 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43a63d477c0edce76d183dee6bccaaf00476e27d
4
+ data.tar.gz: b0312d5bb40ce4ab95cd36b5074f844308049a88
5
+ SHA512:
6
+ metadata.gz: c9542371859bc731095c76cb0b1eba9defd3b301ceeb80def74eabff0f0ae4b35985ed09c8e41e9268212b7cbd6e67c36b138cb5f1382a891435e1c08f1d751f
7
+ data.tar.gz: 5ea9bf4824ac2f8c3a80618c45d59d3c7d8da19b7a9359e5edc1d5e856c0ccba51ad7562060d698aea451011adb1620f7f324fdd4461375167419a376f09f2e7
@@ -0,0 +1,5 @@
1
+ require 'matrix'
2
+ require 'RMagick'
3
+
4
+ require_relative 'openscad-text/text'
5
+ require_relative 'openscad-text/image'
@@ -0,0 +1,30 @@
1
+ # Extend the Magick::Image class with a pixel_matrix method which
2
+ # returns a matrix with pixels represented by :black and :white
3
+ class Magick::Image
4
+ # creates a matrix of the pixels, exchanging them
5
+ # with :black and :white symbol objects
6
+ def pixel_matrix
7
+ # Because I messed things a little up the image needs to be flipped,
8
+ # in order to render the text not mirror-inverted
9
+ flip!
10
+
11
+ pixels = []
12
+ each_pixel do |pixel,_,row|
13
+ # black pixel -> :black, white pixel -> :white
14
+ if pixel.to_color == 'white'
15
+ pixel = :white
16
+ else
17
+ pixel = :black
18
+ end
19
+
20
+ # create a 2-dimensional array
21
+ if pixels[row]
22
+ pixels[row] << pixel
23
+ else
24
+ pixels[row] = [pixel]
25
+ end
26
+ end
27
+
28
+ Matrix[*pixels].transpose
29
+ end
30
+ end
@@ -0,0 +1,235 @@
1
+ class Text
2
+ # This yields 8 vectors turned counter-clockwise 45 degrees each
3
+ class TurnVector
4
+ include Enumerable
5
+ VECTORS = [
6
+ Vector[-1, 0],
7
+ Vector[-1,-1],
8
+ Vector[ 0,-1],
9
+ Vector[ 1,-1],
10
+ Vector[ 1, 0],
11
+ Vector[ 1, 1],
12
+ Vector[ 0, 1],
13
+ Vector[-1, 1],
14
+ ].freeze
15
+
16
+ def initialize(state)
17
+ @state = VECTORS.find_index Vector[*state]
18
+ end
19
+
20
+ def each
21
+ VECTORS.length.times do
22
+ @state = 0 if @state >= VECTORS.length
23
+ yield VECTORS[@state]
24
+ @state += 1
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Represents a Text with a font
31
+ class Text
32
+ include Magick
33
+
34
+ FONT_DIRECTORIES = ["/usr/share/fonts"]
35
+
36
+ attr_reader :draw
37
+
38
+ private
39
+
40
+ def initialize(text, font_path=nil)
41
+ @text = text
42
+
43
+ # setup the draw
44
+ @draw = Draw.new
45
+ @draw.font = font_path || Text.available_fonts.sample
46
+ @draw.gravity = CenterGravity
47
+ @draw.pointsize = 128
48
+ end
49
+
50
+ def create_image
51
+ # calculate and store the image dimensions
52
+ dimensions = @draw.get_type_metrics @text
53
+ @x = dimensions.width.ceil
54
+ @y = dimensions.height.ceil
55
+
56
+ # create the image and draw text
57
+ @image = Image.new(@x, @y) { self.background_color = 'white' }
58
+ @draw.annotate(@image, *[0]*4, @text)
59
+
60
+ @image
61
+ end
62
+
63
+ # checks if a point is already in the points ary
64
+ # aka if it has already been used and also if a point is surrounded
65
+ # by too many other black points
66
+ def point_invalid?(x,y)
67
+ # white points are always invalid
68
+ return true if @matrix[x,y] == :white
69
+
70
+ # point already taken
71
+ return true if @points.any? { |point| point == [x,y] }
72
+
73
+ # if not already taken border points are always valid
74
+ return false if x == 0 or y == 0 or x == @x or y == @y
75
+
76
+ # if all non-diagonal neighbours are black, the point must be invalid
77
+ neighbours = find_direct_neighbours(x,y)
78
+ return true if neighbours.count == 4 and neighbours.all? { |neighbour| @matrix[*neighbour] == :black }
79
+
80
+ # point is valid
81
+ false
82
+ end
83
+
84
+ def find_direct_neighbours(x,y)
85
+ x_min = [x-1, 0].max
86
+ x_max = [x+1, @x].min
87
+
88
+ y_min = [y-1, 0].max
89
+ y_max = [y+1, @y].min
90
+
91
+ [
92
+ [x_min, y ],
93
+ [x_max, y ],
94
+ [x , y_max],
95
+ [x , y_min]
96
+ ].uniq - [[x,y]]
97
+ end
98
+
99
+ def find_next_point(last, current)
100
+ state = Vector[*last] - Vector[*current]
101
+ vecs = TurnVector.new(state).to_a
102
+
103
+ # are we going in the wrong direction?
104
+ #vecs.reverse! if @matrix[*(Vector[*current]+vecs[1]).to_a] == :black
105
+
106
+ # turn the vector and find each which touches a white pixel
107
+ last = :black
108
+ touchy_vecs = vecs.map.with_index do |vec,i|
109
+ color = @matrix[*(Vector[*current]+vec).to_a]
110
+ color_changed = color != last
111
+ last = vec
112
+
113
+ if color_changed
114
+ # return the black point of the two touching the borderline
115
+ color == :black ? vec : vecs[i-1]
116
+ end
117
+ end
118
+
119
+ #remove nil(s) and duplicates
120
+ touchy_vecs.compact!.uniq!
121
+
122
+ # possible next points
123
+ touchy_points = touchy_vecs.map { |vec| (Vector[*current] + vec).to_a }
124
+
125
+ # remove the invalid ones
126
+ touchy_points.delete_if { |point| point_invalid? *point }
127
+
128
+ # return the next point or nil
129
+ touchy_points[0]
130
+ end
131
+
132
+ # finds one possible last point from current_point
133
+ def find_last_point(current_point)
134
+ t = TurnVector.new([-1,-1]).to_a
135
+
136
+ # ary with bools true for :black, false for :white
137
+ is_black = t.map { |vec| @matrix[*(Vector[*current_point]+vec).to_a] == :black }
138
+
139
+ possible_vecs = []
140
+ is_black.each_cons(2).with_index do |cons,i|
141
+ a,b = cons
142
+ if ! a and b
143
+ possible_vecs << t[i+1]
144
+ elsif a and ! b
145
+ possible_vecs << t[i]
146
+ end
147
+ end
148
+
149
+ # corner cases for [false, false, true, true]
150
+ # and [true, true, false, false]
151
+ # then the first/last one is also a border point
152
+ possible_vecs << t.first if ! is_black.first and is_black.last
153
+ possible_vecs << t.last if is_black.first and ! is_black.last
154
+
155
+ # return last point
156
+ (possible_vecs.first + Vector[*current_point]).to_a
157
+ end
158
+
159
+ # starting with point(x,y), try to create a path (or chain)
160
+ # until the starting point is reached again
161
+ def create_pixel_chain(x,y)
162
+ # can't create a chain if the point is invalid
163
+ return if point_invalid?(x,y)
164
+
165
+ # create a new ary in the faces ary
166
+ @paths << []
167
+
168
+ # setup state
169
+ current_point = [x,y]
170
+ last_point = find_last_point(current_point)
171
+
172
+ while current_point
173
+ # add the point to the points array
174
+ @points << current_point
175
+
176
+ # add the index of the last point in points aka current_point to faces
177
+ @paths.last << @points.count - 1
178
+
179
+ # try to find next point (nil if none was found)
180
+ next_point = find_next_point(last_point, current_point)
181
+ last_point = current_point
182
+ current_point = next_point
183
+ end
184
+ end
185
+
186
+ # aligns the text to the bottom-left corner of the first quadrant
187
+ def align_points
188
+ x_min = @points.map { |x,_| x }.min
189
+ y_min = @points.map { |_,y| y }.min
190
+
191
+ @points.map! { |x,y| [x-x_min, y-y_min] }
192
+ end
193
+
194
+ public
195
+ def to_openscad
196
+ @points = []
197
+ @paths = []
198
+
199
+ # create a matrix from pixels
200
+ @matrix = create_image.pixel_matrix
201
+
202
+ # go through each point aka pixel to make sure it gets used once
203
+ @matrix.each_with_index do |_,x,y|
204
+ create_pixel_chain(x,y)
205
+ end
206
+
207
+ # align it!
208
+ align_points
209
+
210
+ # finished woop woop
211
+ "polygon(points=#{@points.to_s}, paths=#{@paths.to_s});"
212
+ end
213
+
214
+ =begin !!just for debugging!!
215
+ def debug_output
216
+ out = @paths.map.with_index do |chain,i|
217
+ s = ""
218
+ s << '# ' if i != 0
219
+
220
+ chain.each { |p_i| s << "translate(#{@points[p_i].to_s}) cube(1);\n" }
221
+ s
222
+ end
223
+
224
+ out.each { |o| puts o, "/"*30 }
225
+ exit
226
+ end
227
+ =end
228
+
229
+ def self.available_fonts
230
+ FONT_DIRECTORIES.map do |dir|
231
+ Dir[dir + "/**/*.ttf"]
232
+ end.flatten!
233
+ end
234
+ end
235
+
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openscad-text
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Florian Lackner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rmagick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ description: A text-generator for Openscad
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/openscad-text.rb
34
+ - lib/openscad-text/image.rb
35
+ - lib/openscad-text/text.rb
36
+ homepage:
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 2.2.2
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Create openscad texts easily
60
+ test_files: []