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.
- checksums.yaml +7 -0
- data/lib/openscad-text.rb +5 -0
- data/lib/openscad-text/image.rb +30 -0
- data/lib/openscad-text/text.rb +235 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -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,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: []
|