fml 0.2.1
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.
- data/.gitignore +8 -0
- data/README +1 -0
- data/Rakefile +3 -0
- data/bin/fml2dae.rb +15 -0
- data/bin/fml2obj.rb +10 -0
- data/fml.gemspec +36 -0
- data/lib/collada/document.rb +101 -0
- data/lib/collada/geometry.rb +110 -0
- data/lib/config.yml +17 -0
- data/lib/floorplanner/area_builder.rb +42 -0
- data/lib/floorplanner/asset.rb +185 -0
- data/lib/floorplanner/collada_export.rb +118 -0
- data/lib/floorplanner/design.rb +108 -0
- data/lib/floorplanner/document.rb +68 -0
- data/lib/floorplanner/obj_export.rb +24 -0
- data/lib/floorplanner/opening3d.rb +140 -0
- data/lib/floorplanner/rib_export.rb +24 -0
- data/lib/floorplanner/svg_export.rb +25 -0
- data/lib/floorplanner/wall3d.rb +97 -0
- data/lib/floorplanner/wall_builder.rb +165 -0
- data/lib/floorplanner.rb +52 -0
- data/lib/geom/connection.rb +14 -0
- data/lib/geom/ear_trim.rb +52 -0
- data/lib/geom/edge.rb +89 -0
- data/lib/geom/glu_tess.rb +34 -0
- data/lib/geom/intersection.rb +38 -0
- data/lib/geom/matrix3d.rb +141 -0
- data/lib/geom/number.rb +104 -0
- data/lib/geom/plane.rb +36 -0
- data/lib/geom/polygon.rb +264 -0
- data/lib/geom/triangle.rb +38 -0
- data/lib/geom/triangle_mesh.rb +94 -0
- data/lib/geom/vertex.rb +33 -0
- data/lib/geom.rb +13 -0
- data/lib/keyhole/archive.rb +36 -0
- data/tasks/github-gem.rake +315 -0
- data/views/design.dae.erb +439 -0
- data/views/design.obj.erb +17 -0
- data/views/design.rib.erb +17 -0
- data/views/design.svg.erb +42 -0
- data/xml/collada_schema_1_4.xsd +11046 -0
- data/xml/fml.rng +268 -0
- data/xml/fml2kml.xsl +59 -0
- metadata +117 -0
data/lib/geom/polygon.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
module Geom
|
2
|
+
class Polygon < TriangleMesh
|
3
|
+
|
4
|
+
WINDING_CW = 0
|
5
|
+
WINDING_CCW = 1
|
6
|
+
|
7
|
+
AXIS_X = 1
|
8
|
+
AXIS_Y = 2
|
9
|
+
AXIS_Z = 3
|
10
|
+
|
11
|
+
CAP_TOP = 4
|
12
|
+
CAP_BASE = 5
|
13
|
+
CAP_BOTH = 6
|
14
|
+
|
15
|
+
def clone
|
16
|
+
vertices = @vertices.collect{|v| v.clone}
|
17
|
+
texcoord = @texcoord ? @texcoord.collect{|uv| uv.clone} : nil
|
18
|
+
faces = @faces.collect do |f|
|
19
|
+
if texcoord
|
20
|
+
Triangle.new([
|
21
|
+
vertices[ @vertices.index(f.vertices[0]) ],
|
22
|
+
vertices[ @vertices.index(f.vertices[1]) ],
|
23
|
+
vertices[ @vertices.index(f.vertices[2]) ]
|
24
|
+
],[
|
25
|
+
texcoord[ @texcoord.index(f.texcoord[0]) ],
|
26
|
+
texcoord[ @texcoord.index(f.texcoord[1]) ],
|
27
|
+
texcoord[ @texcoord.index(f.texcoord[2]) ]
|
28
|
+
])
|
29
|
+
else
|
30
|
+
Triangle.new([
|
31
|
+
vertices[ @vertices.index(f.vertices[0]) ],
|
32
|
+
vertices[ @vertices.index(f.vertices[1]) ],
|
33
|
+
vertices[ @vertices.index(f.vertices[2]) ]
|
34
|
+
])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
result = Polygon.new(vertices,faces,@data.dup)
|
38
|
+
result.texcoord = texcoord
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def update
|
43
|
+
return false if @vertices.length < 3
|
44
|
+
triangles = @tess.triangulate(self)
|
45
|
+
unless triangles
|
46
|
+
@vertices.reverse!
|
47
|
+
triangles = @tess.triangulate(self)
|
48
|
+
return false unless triangles
|
49
|
+
end
|
50
|
+
|
51
|
+
@texcoord = calc_uv
|
52
|
+
triangles.each do |t|
|
53
|
+
v0 = @vertices[t[0]]
|
54
|
+
v1 = @vertices[t[1]]
|
55
|
+
v2 = @vertices[t[2]]
|
56
|
+
|
57
|
+
t0 = @texcoord[t[0]]
|
58
|
+
t1 = @texcoord[t[1]]
|
59
|
+
t2 = @texcoord[t[2]]
|
60
|
+
@faces.push(Triangle.new([v2,v1,v0],[t0,t1,t2]))
|
61
|
+
end
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
def area
|
66
|
+
# remove duplicates and invisibles
|
67
|
+
return nil if @vertices.length < 3
|
68
|
+
@vertices.each{|v| return 0 if @vertices.grep(v).length>1}
|
69
|
+
|
70
|
+
result = 0
|
71
|
+
points = @vertices.dup
|
72
|
+
plane = self.plane
|
73
|
+
|
74
|
+
ax = plane.normal.x > 0 ? plane.normal.x : -plane.normal.x
|
75
|
+
ay = plane.normal.y > 0 ? plane.normal.y : -plane.normal.y
|
76
|
+
az = plane.normal.z > 0 ? plane.normal.z : -plane.normal.z
|
77
|
+
|
78
|
+
coord = AXIS_Z
|
79
|
+
|
80
|
+
if ax > ay
|
81
|
+
if ax > az
|
82
|
+
coord = AXIS_X
|
83
|
+
end
|
84
|
+
elsif ay > az
|
85
|
+
coord = AXIS_Y
|
86
|
+
end
|
87
|
+
|
88
|
+
points.push(points[0],points[1])
|
89
|
+
|
90
|
+
# compute area of the 2D projection
|
91
|
+
points.each_with_index do |point,i|
|
92
|
+
next if i.zero?
|
93
|
+
j = (i+1) % points.length
|
94
|
+
k = (i-1) % points.length
|
95
|
+
|
96
|
+
case coord
|
97
|
+
when AXIS_X
|
98
|
+
result += (point.y * (points[j].z - points[k].z))
|
99
|
+
when AXIS_Y
|
100
|
+
result += (point.x * (points[j].z - points[k].z))
|
101
|
+
else
|
102
|
+
result += (point.x * (points[j].y - points[k].y))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# scale to get area before projection
|
107
|
+
an = Math.sqrt(ax**2 + ay**2 + az**2) # length of normal vector
|
108
|
+
case coord
|
109
|
+
when AXIS_X
|
110
|
+
result *= (an / (2*ax))
|
111
|
+
when AXIS_Y
|
112
|
+
result *= (an / (2*ay))
|
113
|
+
when AXIS_Z
|
114
|
+
result *= (an / (2*az))
|
115
|
+
end
|
116
|
+
2.times {points.pop}
|
117
|
+
result
|
118
|
+
end
|
119
|
+
|
120
|
+
# TODO real plane, not just from first 3 vertices
|
121
|
+
def plane
|
122
|
+
return nil if @vertices.length < 3
|
123
|
+
Plane.three_points(*@vertices[0..2])
|
124
|
+
end
|
125
|
+
|
126
|
+
def point_inside(pt)
|
127
|
+
x = "x"
|
128
|
+
y = "y"
|
129
|
+
n = @vertices.length
|
130
|
+
dominant = dominant_axis
|
131
|
+
dist = self.plane.distance(pt)
|
132
|
+
result = false
|
133
|
+
|
134
|
+
return false if dist.abs > 0.01
|
135
|
+
case dominant
|
136
|
+
when AXIS_X
|
137
|
+
x = "y"
|
138
|
+
y = "z"
|
139
|
+
when AXIS_Y
|
140
|
+
y = "z"
|
141
|
+
end
|
142
|
+
|
143
|
+
@vertices.each_with_index do |v,i|
|
144
|
+
vn = @vertices[(i+1)%n] # next
|
145
|
+
if (((v.send(y) <= pt.send(y)) && (pt.send(y) < vn.send(y))) || ((vn.send(y) <= pt.send(y)) && (pt.send(y) < v.send(y)))) &&
|
146
|
+
(pt.send(x) < (vn.send(x) - v.send(x)) * (pt.send(y) - v.send(y)) / (vn.send(y) - v.send(y)) + v.send(x))
|
147
|
+
result = !result
|
148
|
+
end
|
149
|
+
end
|
150
|
+
result
|
151
|
+
end
|
152
|
+
|
153
|
+
def winding
|
154
|
+
area < 0 ? WINDING_CW : WINDING_CCW
|
155
|
+
end
|
156
|
+
|
157
|
+
def extrude(distance,direction,cap=CAP_BOTH,update=true)
|
158
|
+
direction.normalize
|
159
|
+
top_cap = clone
|
160
|
+
top_cap.vertices.each do |v|
|
161
|
+
v.x += distance*direction.x
|
162
|
+
v.y += distance*direction.y
|
163
|
+
v.z += distance*direction.z
|
164
|
+
end
|
165
|
+
top_cap.faces.each {|f| f.normal = direction }
|
166
|
+
num = @vertices.length
|
167
|
+
|
168
|
+
sides = Array.new(@vertices.length).map!{ Polygon.new }
|
169
|
+
sides.each_with_index do |side,i|
|
170
|
+
j = (i+1) % num
|
171
|
+
side.vertices.push(top_cap.vertices[i],top_cap.vertices[j])
|
172
|
+
side.vertices.push(@vertices[j],@vertices[i])
|
173
|
+
side.data[:side] = true
|
174
|
+
side.update if update
|
175
|
+
end
|
176
|
+
|
177
|
+
case cap
|
178
|
+
when CAP_BASE
|
179
|
+
top_cap.faces.clear
|
180
|
+
when CAP_TOP
|
181
|
+
self.faces.clear
|
182
|
+
when CAP_BOTH
|
183
|
+
self.faces.each do |f|
|
184
|
+
f.flip_normal
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
sides + [top_cap]
|
189
|
+
end
|
190
|
+
|
191
|
+
def dominant_axis
|
192
|
+
plane = self.plane
|
193
|
+
return 0 unless plane
|
194
|
+
|
195
|
+
ax = (plane.normal.x > 0 ? plane.normal.x : -plane.normal.x)
|
196
|
+
ay = (plane.normal.y > 0 ? plane.normal.y : -plane.normal.y)
|
197
|
+
az = (plane.normal.z > 0 ? plane.normal.z : -plane.normal.z)
|
198
|
+
|
199
|
+
axis = AXIS_Z
|
200
|
+
if ax > ay
|
201
|
+
if ax > az
|
202
|
+
axis = AXIS_X
|
203
|
+
end
|
204
|
+
elsif ay > az
|
205
|
+
axis = AXIS_Y
|
206
|
+
end
|
207
|
+
axis
|
208
|
+
end
|
209
|
+
|
210
|
+
def calc_uv
|
211
|
+
result = []
|
212
|
+
plane = self.plane
|
213
|
+
up = Number3D.new( 0, 1, 0 )
|
214
|
+
|
215
|
+
# get side vector
|
216
|
+
side = Number3D.cross(up, plane.normal)
|
217
|
+
side.normalize
|
218
|
+
|
219
|
+
# adjust up vector
|
220
|
+
up = Number3D.cross(self.plane.normal, side)
|
221
|
+
up.normalize
|
222
|
+
|
223
|
+
matrix = Matrix3D[
|
224
|
+
[side.x, up.x, plane.normal.x, 0],
|
225
|
+
[side.y, up.y, plane.normal.y, 0],
|
226
|
+
[side.z, up.z, plane.normal.z, 0],
|
227
|
+
[0, 0, 0, 1]]
|
228
|
+
|
229
|
+
v, n, t = nil, nil, nil
|
230
|
+
min = Number3D.new(1000,1000,1000)
|
231
|
+
max = Number3D.new(-min.x, -min.y, -min.z)
|
232
|
+
pts = []
|
233
|
+
|
234
|
+
@vertices.each do |v|
|
235
|
+
n = v.position
|
236
|
+
|
237
|
+
# Matrix3D.multiplyVector3x3( matrix, n );
|
238
|
+
|
239
|
+
min.x = n.x if n.x < min.x
|
240
|
+
min.y = n.y if n.y < min.y
|
241
|
+
max.x = n.x if n.x > max.x
|
242
|
+
max.y = n.y if n.y > max.y
|
243
|
+
|
244
|
+
pts << n
|
245
|
+
result << NumberUV.new
|
246
|
+
end
|
247
|
+
|
248
|
+
w = max.x - min.x
|
249
|
+
h = max.y - min.y
|
250
|
+
size = w < h ? h : w
|
251
|
+
|
252
|
+
@vertices.each_with_index do |v,i|
|
253
|
+
n = pts[i]
|
254
|
+
t = result[i]
|
255
|
+
|
256
|
+
t.u = ((n.x - min.x) / size) * size
|
257
|
+
t.v = ((n.y - min.y) / size) * size
|
258
|
+
end
|
259
|
+
|
260
|
+
result
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Geom
|
2
|
+
class Triangle
|
3
|
+
|
4
|
+
attr_accessor :vertices, :texcoord, :normal
|
5
|
+
|
6
|
+
def initialize(vertices,texcoord=nil,normal=nil)
|
7
|
+
unless vertices.length == 3
|
8
|
+
raise "Triangle must consist of exactly 3 vertices"
|
9
|
+
end
|
10
|
+
@normal = normal || Number3D.new
|
11
|
+
@vertices = vertices
|
12
|
+
@texcoord = texcoord
|
13
|
+
create_normal
|
14
|
+
end
|
15
|
+
|
16
|
+
def flip_normal
|
17
|
+
@normal.x = -@normal.x
|
18
|
+
@normal.y = -@normal.y
|
19
|
+
@normal.z = -@normal.z
|
20
|
+
end
|
21
|
+
|
22
|
+
def clone
|
23
|
+
Triangle.new(@vertices.collect{|v| v.clone},@texcoord.collect{|uv| uv.clone})
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def create_normal
|
28
|
+
vn0 = @vertices[0].position.clone
|
29
|
+
vn1 = @vertices[1].position.clone
|
30
|
+
vn2 = @vertices[2].position.clone
|
31
|
+
vn1.minus_eq(vn0)
|
32
|
+
vn2.minus_eq(vn0)
|
33
|
+
@normal = Number3D.cross(vn1,vn2,@normal)
|
34
|
+
@normal.normalize
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Geom
|
2
|
+
class TriangleMesh
|
3
|
+
|
4
|
+
attr_accessor(:vertices,:texcoord,:faces,:meshes,:data,:tess)
|
5
|
+
|
6
|
+
def initialize( vertices=nil, faces=nil, user_data=nil )
|
7
|
+
@data = user_data || Hash.new
|
8
|
+
@faces = faces || Array.new
|
9
|
+
@vertices = vertices || Array.new
|
10
|
+
@meshes = Array.new
|
11
|
+
|
12
|
+
@tess = EarTrim
|
13
|
+
end
|
14
|
+
|
15
|
+
def << (mesh)
|
16
|
+
@meshes << mesh
|
17
|
+
end
|
18
|
+
|
19
|
+
def vertices
|
20
|
+
result = @meshes.collect {|m| m.vertices}.flatten
|
21
|
+
result.empty? ? @vertices : result
|
22
|
+
end
|
23
|
+
|
24
|
+
def faces
|
25
|
+
result = @meshes.collect {|m| m.faces}.flatten
|
26
|
+
result.empty? ? @faces : result
|
27
|
+
end
|
28
|
+
|
29
|
+
def texcoord
|
30
|
+
result = @meshes.collect {|m| m.texcoord}.flatten
|
31
|
+
result.empty? ? @texcoord : result
|
32
|
+
end
|
33
|
+
|
34
|
+
def update
|
35
|
+
@meshes.each {|m| m.update}
|
36
|
+
end
|
37
|
+
|
38
|
+
def reverse
|
39
|
+
faces.each {|f| f.vertices.reverse! }
|
40
|
+
end
|
41
|
+
|
42
|
+
def transform_vertices(transformation)
|
43
|
+
ta = transformation.to_a
|
44
|
+
m11 = ta[0][0]
|
45
|
+
m12 = ta[0][1]
|
46
|
+
m13 = ta[0][2]
|
47
|
+
m21 = ta[1][0]
|
48
|
+
m22 = ta[1][1]
|
49
|
+
m23 = ta[1][2]
|
50
|
+
m31 = ta[2][0]
|
51
|
+
m32 = ta[2][1]
|
52
|
+
m33 = ta[2][2]
|
53
|
+
|
54
|
+
m14 = ta[0][3]
|
55
|
+
m24 = ta[1][3]
|
56
|
+
m34 = ta[2][3]
|
57
|
+
|
58
|
+
@vertices.each do |v|
|
59
|
+
vx = v.x
|
60
|
+
vy = v.y
|
61
|
+
vz = v.z
|
62
|
+
|
63
|
+
tx = vx * m11 + vy * m12 + vz * m13 + m14
|
64
|
+
ty = vx * m21 + vy * m22 + vz * m23 + m24
|
65
|
+
tz = vx * m31 + vy * m32 + vz * m33 + m34
|
66
|
+
|
67
|
+
v.x = tx
|
68
|
+
v.y = ty
|
69
|
+
v.z = tz
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def bounding_box
|
74
|
+
min = Geom::Number3D.new( 1000, 1000, 1000)
|
75
|
+
max = Geom::Number3D.new(-1000,-1000,-1000)
|
76
|
+
|
77
|
+
vertices.each do |v|
|
78
|
+
min.x = v.x if v.x < min.x
|
79
|
+
min.y = v.y if v.y < min.y
|
80
|
+
min.z = v.z if v.z < min.z
|
81
|
+
|
82
|
+
max.x = v.x if v.x > max.x
|
83
|
+
max.y = v.y if v.y > max.y
|
84
|
+
max.z = v.z if v.z > max.z
|
85
|
+
end
|
86
|
+
|
87
|
+
{ :max => max , :min => min }
|
88
|
+
end
|
89
|
+
|
90
|
+
def merge(other)
|
91
|
+
@vertices.concat other.vertices
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/geom/vertex.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Geom
|
4
|
+
class Vertex
|
5
|
+
attr_accessor(:position,:normal)
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators(:@position,
|
8
|
+
:x, :y, :z, :x=, :y=, :z=,
|
9
|
+
:distance_x, :distance_y, :distance_z, :distance, :to_floats)
|
10
|
+
def initialize(x=0.0,y=0.0,z=0.0,normal=nil)
|
11
|
+
@position = Number3D.new(x,y,z)
|
12
|
+
@normal = normal
|
13
|
+
end
|
14
|
+
|
15
|
+
def == (other)
|
16
|
+
@position == other.position
|
17
|
+
end
|
18
|
+
|
19
|
+
def equal?(other,snap)
|
20
|
+
@position.x-snap < other.x && @position.x+snap > other.x &&
|
21
|
+
@position.y-snap < other.y && @position.y+snap > other.y &&
|
22
|
+
@position.z-snap < other.z && @position.z+snap > other.z
|
23
|
+
end
|
24
|
+
|
25
|
+
def clone
|
26
|
+
Vertex.new(x,y,z)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#<Geom::Vertex:#{@position.to_s}>"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/geom.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.push(File.dirname(__FILE__))
|
2
|
+
require 'matrix'
|
3
|
+
require 'geom/number'
|
4
|
+
require 'geom/vertex'
|
5
|
+
require 'geom/triangle_mesh'
|
6
|
+
require 'geom/polygon'
|
7
|
+
require 'geom/triangle'
|
8
|
+
require 'geom/plane'
|
9
|
+
require 'geom/matrix3d'
|
10
|
+
require 'geom/intersection'
|
11
|
+
require 'geom/connection'
|
12
|
+
require 'geom/edge'
|
13
|
+
require 'geom/ear_trim'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Keyhole
|
2
|
+
class Archive < Zip::ZipFile
|
3
|
+
DAE_CACHE_PATH = File.join(Floorplanner.config['dae_cache_path'],'dae')
|
4
|
+
IMG_CACHE_PATH = File.join(Floorplanner.config['dae_cache_path'],'textures')
|
5
|
+
|
6
|
+
def dae_path(asset_id)
|
7
|
+
FileUtils.mkdir_p DAE_CACHE_PATH
|
8
|
+
dae = entries.select{|e| e.name.match(/\.dae$/)}.first
|
9
|
+
@relative_dae_path = dae.name
|
10
|
+
@dae_path = File.join(
|
11
|
+
DAE_CACHE_PATH,
|
12
|
+
"#{asset_id}_#{File.basename(dae.name)}"
|
13
|
+
)
|
14
|
+
dae.extract(@dae_path) unless File.exists?(@dae_path)
|
15
|
+
@dae_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def image_path(asset_id,relative_to_dae,relative_out=false)
|
19
|
+
FileUtils.mkdir_p IMG_CACHE_PATH
|
20
|
+
img_path = File.join(File.dirname(@relative_dae_path),relative_to_dae)
|
21
|
+
target_path = File.join(IMG_CACHE_PATH,asset_id)
|
22
|
+
tex_path = File.join(target_path,File.basename(img_path))
|
23
|
+
|
24
|
+
unless File.exists?(tex_path)
|
25
|
+
FileUtils.mkdir_p target_path
|
26
|
+
zip_image = entries.select{|e| e.name.match(File.basename(img_path)) }.first
|
27
|
+
extract( zip_image , tex_path )
|
28
|
+
end
|
29
|
+
relative_out ? '../textures'+tex_path.gsub(IMG_CACHE_PATH,'') : tex_path
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy
|
33
|
+
File.unlink(@dae_path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|