openscad-text 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []