floorplanner-fml 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +1 -0
- data/bin/fml2dae.rb +15 -0
- data/bin/fml2obj.rb +10 -0
- data/lib/collada/document.rb +101 -0
- data/lib/collada/geometry.rb +110 -0
- data/lib/config.yml +15 -0
- data/lib/floorplanner.rb +46 -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 +107 -0
- data/lib/floorplanner/document.rb +66 -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/geom.rb +13 -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/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/keyhole/archive.rb +36 -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
- metadata +110 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Document
|
3
|
+
|
4
|
+
def to_rib(design_id,out_path)
|
5
|
+
@design = Design.new(@xml,design_id)
|
6
|
+
@design.build_geometries
|
7
|
+
rib = File.new(out_path,'w')
|
8
|
+
rib.write @design.to_rib
|
9
|
+
rib.close
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
module RibExport
|
15
|
+
def to_rib
|
16
|
+
raise "No geometries to export. Call build_geometries first" unless @areas && @walls
|
17
|
+
|
18
|
+
template = ERB.new(
|
19
|
+
File.read(
|
20
|
+
File.join(Floorplanner.config['views_path'],'design.rib.erb')))
|
21
|
+
template.result(binding)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
module SvgExport
|
3
|
+
def to_svg
|
4
|
+
# translate to x:0,y:0
|
5
|
+
bbox = @walls.bounding_box
|
6
|
+
dx = bbox[:min].distance_x(bbox[:max])
|
7
|
+
dy = bbox[:min].distance_y(bbox[:max])
|
8
|
+
min_x = -bbox[:min].x
|
9
|
+
min_y = -bbox[:min].y
|
10
|
+
# fit into document dimensions
|
11
|
+
width , height , padding = Floorplanner.config['svg']['width'],
|
12
|
+
Floorplanner.config['svg']['height'],
|
13
|
+
Floorplanner.config['svg']['padding']
|
14
|
+
ratio = ( width < height ? width : height ) * padding / ( dx > dy ? dx : dy )
|
15
|
+
# center on stage
|
16
|
+
mod_x = min_x + (width /ratio)/2 - dx/2
|
17
|
+
mod_y = min_y + (height/ratio)/2 - dy/2
|
18
|
+
|
19
|
+
template = ERB.new(
|
20
|
+
File.read(
|
21
|
+
File.join(Floorplanner.config['views_path'],'design.svg.erb')))
|
22
|
+
template.result(binding)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Wall3D < Geom::TriangleMesh
|
3
|
+
UP = Geom::Number3D.new(0,0,1)
|
4
|
+
attr_accessor(:baseline,:outline,:inner,:outer,:name)
|
5
|
+
def initialize(baseline,thickness,height,name)
|
6
|
+
super()
|
7
|
+
@baseline = baseline
|
8
|
+
@thickness = thickness
|
9
|
+
@height = height
|
10
|
+
@name = name
|
11
|
+
|
12
|
+
# create inner and outer Edges of wall
|
13
|
+
@inner = @baseline.offset(@thickness/2.0,UP)
|
14
|
+
@outer = @baseline.offset(-@thickness/2.0,UP)
|
15
|
+
@openings = Array.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def opening(position,size,type)
|
19
|
+
@openings << {:position => position, :size => size, :type => type}
|
20
|
+
end
|
21
|
+
|
22
|
+
def windows
|
23
|
+
@openings.collect{|o| o.window}.compact
|
24
|
+
end
|
25
|
+
|
26
|
+
# create base 'outline' polygon of wall
|
27
|
+
def prepare(num_start_connections,num_end_connections)
|
28
|
+
@outline = Geom::Polygon.new
|
29
|
+
if num_start_connections == 1 || num_start_connections == 2
|
30
|
+
@outline.vertices.push(
|
31
|
+
@outer.start_point,
|
32
|
+
@inner.start_point)
|
33
|
+
else
|
34
|
+
@outline.vertices.push(
|
35
|
+
@outer.start_point,
|
36
|
+
@baseline.start_point,
|
37
|
+
@inner.start_point)
|
38
|
+
end
|
39
|
+
|
40
|
+
if num_end_connections == 1 || num_end_connections == 2
|
41
|
+
@outline.vertices.push(
|
42
|
+
@inner.end_point,
|
43
|
+
@outer.end_point)
|
44
|
+
else
|
45
|
+
@outline.vertices.push(
|
46
|
+
@inner.end_point,
|
47
|
+
@baseline.end_point,
|
48
|
+
@outer.end_point)
|
49
|
+
end
|
50
|
+
@outline.vertices.reverse!
|
51
|
+
@outline.data[:color] = "#ff9999"
|
52
|
+
end
|
53
|
+
|
54
|
+
def update
|
55
|
+
@openings.each_with_index do |opening,i|
|
56
|
+
op = Opening3D.new(@baseline,@thickness,opening)
|
57
|
+
op.update
|
58
|
+
@meshes << op
|
59
|
+
@openings[i] = op
|
60
|
+
end
|
61
|
+
@openings = @openings.sort_by{|o| o.position.distance(@baseline.start_point.position)}
|
62
|
+
@outline.update
|
63
|
+
@meshes << @outline
|
64
|
+
|
65
|
+
# create top cap for wall
|
66
|
+
top_cap = @outline.clone
|
67
|
+
top_cap.transform_vertices(Geom::Matrix3D.translation(0,0,@height))
|
68
|
+
@meshes << top_cap
|
69
|
+
# flip bottom cap
|
70
|
+
@outline.reverse
|
71
|
+
|
72
|
+
# create walls side polygons
|
73
|
+
num = @outline.vertices.length
|
74
|
+
starts = [@outer.start_point,@inner.start_point,@baseline.start_point]
|
75
|
+
ends = [@outer.end_point, @inner.end_point, @baseline.end_point]
|
76
|
+
outs = [@outer.start_point,@outer.end_point]
|
77
|
+
@outline.vertices.each_with_index do |v,i|
|
78
|
+
j = @outline.vertices[(i+1) % num]
|
79
|
+
# omit starting and ending polygons
|
80
|
+
next if ( starts.include?(v) && starts.include?(j) ) ||
|
81
|
+
( ends.include?(v) && ends.include?(j) )
|
82
|
+
|
83
|
+
edge = Geom::Edge.new(v,j)
|
84
|
+
poly = edge.extrude(@height,UP)
|
85
|
+
mesh = Geom::TriangleMesh.new
|
86
|
+
mesh << poly
|
87
|
+
|
88
|
+
# drill holes to side
|
89
|
+
@openings.each do |opening|
|
90
|
+
opening.drill(mesh,outs.include?(v))
|
91
|
+
end
|
92
|
+
mesh.update
|
93
|
+
@meshes << mesh
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class WallBuilder < Geom::TriangleMesh
|
3
|
+
|
4
|
+
def initialize(&block)
|
5
|
+
super()
|
6
|
+
@connections = Hash.new
|
7
|
+
@base_vertices = Array.new
|
8
|
+
@walls = Array.new
|
9
|
+
block.call(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
@walls.each{|w| block.call(w)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def collect(&block)
|
17
|
+
@walls.collect{|w| block.call(w)}
|
18
|
+
end
|
19
|
+
|
20
|
+
def vertex(vertex)
|
21
|
+
if existing = find_vertex(@base_vertices,vertex,Floorplanner.config['geom_snap'])
|
22
|
+
existing
|
23
|
+
else
|
24
|
+
@base_vertices << vertex
|
25
|
+
vertex
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def wall(sp,ep,thickness,height)
|
30
|
+
@connections[sp] = Array.new unless @connections.include?(sp)
|
31
|
+
@connections[ep] = Array.new unless @connections.include?(ep)
|
32
|
+
cs = Geom::Connection.new(ep, 0.0)
|
33
|
+
ce = Geom::Connection.new(sp, 0.0)
|
34
|
+
@connections[sp] << cs unless @connections[sp].include?(cs)
|
35
|
+
@connections[ep] << ce unless @connections[ep].include?(ce)
|
36
|
+
@walls << Wall3D.new(Geom::Edge.new(sp,ep), thickness, height, "wall_#{@walls.length}")
|
37
|
+
end
|
38
|
+
|
39
|
+
# call after adding walls
|
40
|
+
def opening(position,size,type)
|
41
|
+
@walls.each do |wall|
|
42
|
+
if wall.outline.point_inside(position)
|
43
|
+
wall.opening(position,size,type)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def prepare
|
49
|
+
@base_vertices.each do |v|
|
50
|
+
connections = @connections[v]
|
51
|
+
next if connections.length.zero?
|
52
|
+
|
53
|
+
connections.each do |c|
|
54
|
+
x = c.point.x - v.x
|
55
|
+
y = c.point.y - v.y
|
56
|
+
c.angle = Math.atan2(y,x)
|
57
|
+
end
|
58
|
+
connections.sort! {|a,b| a.angle <=> b.angle}
|
59
|
+
connections.each_index do |i|
|
60
|
+
j = (i+1) % connections.length
|
61
|
+
|
62
|
+
w0 , w1 = find_wall(v,connections[i].point),
|
63
|
+
find_wall(v,connections[j].point)
|
64
|
+
|
65
|
+
flipped0 , flipped1 = (w0.baseline.end_point == v),
|
66
|
+
(w1.baseline.end_point == v)
|
67
|
+
|
68
|
+
e0 , e1 = flipped0 ? w0.outer : w0.inner,
|
69
|
+
flipped1 ? w1.inner : w1.outer
|
70
|
+
|
71
|
+
isect = Geom::Intersection.line_line(e0.start_point.position,e0.end_point.position,e1.start_point.position,e1.end_point.position,true)
|
72
|
+
|
73
|
+
if isect.status == Geom::Intersection::INTERSECTION
|
74
|
+
# the two edges intersect!
|
75
|
+
# adjust the edges so they touch at the intersection.
|
76
|
+
if isect.alpha[0].abs < 2
|
77
|
+
if flipped0
|
78
|
+
e0.end_point.x = isect.points[0].x
|
79
|
+
e0.end_point.y = isect.points[0].y
|
80
|
+
else
|
81
|
+
e0.start_point.x = isect.points[0].x
|
82
|
+
e0.start_point.y = isect.points[0].y
|
83
|
+
end
|
84
|
+
|
85
|
+
if flipped1
|
86
|
+
e1.end_point.x = isect.points[0].x
|
87
|
+
e1.end_point.y = isect.points[0].y
|
88
|
+
else
|
89
|
+
e1.start_point.x = isect.points[0].x
|
90
|
+
e1.start_point.y = isect.points[0].y
|
91
|
+
end
|
92
|
+
else
|
93
|
+
# parallel
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
@walls.each do |wall|
|
100
|
+
num_start = @connections[wall.baseline.start_point].length
|
101
|
+
num_end = @connections[wall.baseline.end_point].length
|
102
|
+
wall.prepare(num_start,num_end)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def update
|
107
|
+
@walls.each do |wall|
|
108
|
+
# here comes the cache
|
109
|
+
wall.update
|
110
|
+
@vertices.concat(wall.vertices)
|
111
|
+
@faces.concat(wall.faces)
|
112
|
+
end
|
113
|
+
|
114
|
+
$stderr.puts "Walls Vertices before: #{@vertices.length.to_s}"
|
115
|
+
# remove same instances
|
116
|
+
@vertices.uniq!
|
117
|
+
# remove same vertices
|
118
|
+
old = @vertices.dup
|
119
|
+
@vertices = Array.new
|
120
|
+
old.each do |v|
|
121
|
+
@vertices.push(v) unless @vertices.include?(v) # find_vertex(@vertices,v) #
|
122
|
+
end
|
123
|
+
$stderr.puts "Walls Vertices: #{@vertices.length.to_s}"
|
124
|
+
$stderr.puts "Walls Faces : #{@faces.length.to_s}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# make use of cache
|
128
|
+
def vertices
|
129
|
+
@vertices
|
130
|
+
end
|
131
|
+
|
132
|
+
def faces
|
133
|
+
@faces
|
134
|
+
end
|
135
|
+
|
136
|
+
def windows
|
137
|
+
@walls.collect{|w| w.windows}.flatten
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def find_wall(sp,ep)
|
143
|
+
@walls.each do |wall|
|
144
|
+
if wall.baseline.start_point.equal?(sp,Floorplanner.config['geom_snap']) &&
|
145
|
+
wall.baseline.end_point.equal?(ep,Floorplanner.config['geom_snap'])
|
146
|
+
return wall
|
147
|
+
elsif wall.baseline.end_point.equal?(sp,Floorplanner.config['geom_snap']) &&
|
148
|
+
wall.baseline.start_point.equal?(ep,Floorplanner.config['geom_snap'])
|
149
|
+
return wall
|
150
|
+
end
|
151
|
+
end
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def find_vertex(arr,v,snap=Floorplanner.config['uniq_snap'])
|
156
|
+
arr.each do |vertex|
|
157
|
+
if v.equal?(vertex,snap)
|
158
|
+
return vertex
|
159
|
+
end
|
160
|
+
end
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
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,14 @@
|
|
1
|
+
module Geom
|
2
|
+
class Connection
|
3
|
+
alias_method(:==, :equal?)
|
4
|
+
attr_accessor(:point,:angle)
|
5
|
+
def initialize(point,angle)
|
6
|
+
@point = point
|
7
|
+
@angle = angle
|
8
|
+
end
|
9
|
+
|
10
|
+
def equal?(other)
|
11
|
+
other.point == @point && other.angle == @angle
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#
|
2
|
+
# Implements "Ear trimming" triangulation algorithm
|
3
|
+
#
|
4
|
+
module Geom
|
5
|
+
class EarTrim
|
6
|
+
def self.triangulate(poly)
|
7
|
+
result = Array.new
|
8
|
+
points = poly.vertices.dup
|
9
|
+
num = points.length
|
10
|
+
count = num*2
|
11
|
+
indices = Hash.new
|
12
|
+
|
13
|
+
return [ [0,1,2] ] if num == 3
|
14
|
+
points.reverse! if poly.winding == Polygon::WINDING_CW
|
15
|
+
points.each_with_index do |p,i|
|
16
|
+
indices[p] = i
|
17
|
+
end
|
18
|
+
|
19
|
+
while num > 2
|
20
|
+
return nil if count > num*2 # overflow
|
21
|
+
count += 1
|
22
|
+
|
23
|
+
i = 0
|
24
|
+
while i < num
|
25
|
+
j = (i+num-1) % num
|
26
|
+
k = (i+1) % num
|
27
|
+
|
28
|
+
if is_ear(points,j,i,k)
|
29
|
+
# save triangle
|
30
|
+
result.push([indices[points[j]],indices[points[i]],indices[points[k]]])
|
31
|
+
# remove vertex
|
32
|
+
points.delete_at(i)
|
33
|
+
num = points.length
|
34
|
+
count = 0
|
35
|
+
end
|
36
|
+
i += 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.is_ear(points,u,v,w)
|
43
|
+
poly = Polygon.new([points[u],points[v],points[w]])
|
44
|
+
return false if poly.area < 0
|
45
|
+
points.length.times do |i|
|
46
|
+
next if i == u || i == v || i == w
|
47
|
+
return false if poly.point_inside(points[i])
|
48
|
+
end
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/geom/edge.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module Geom
|
2
|
+
class Edge
|
3
|
+
attr_accessor(:start_point,:end_point)
|
4
|
+
def initialize(sp,ep)
|
5
|
+
@start_point = sp
|
6
|
+
@end_point = ep
|
7
|
+
end
|
8
|
+
|
9
|
+
def direction
|
10
|
+
Number3D.sub(@start_point.position,@end_point.position)
|
11
|
+
end
|
12
|
+
|
13
|
+
def length
|
14
|
+
x = @end_point.x - @start_point.x;
|
15
|
+
y = @end_point.y - @start_point.y;
|
16
|
+
z = @end_point.z - @start_point.z;
|
17
|
+
Math.sqrt(x*x + y*y + z*z);
|
18
|
+
end
|
19
|
+
|
20
|
+
def offset(distance,up)
|
21
|
+
up.normalize
|
22
|
+
edge = clone
|
23
|
+
dir = direction
|
24
|
+
|
25
|
+
Matrix3D.multiply_vector_3x3(Matrix3D.rotation_matrix(up.x,up.y,up.z, -Math::PI/2),dir)
|
26
|
+
dir.normalize
|
27
|
+
|
28
|
+
dir.x *= distance
|
29
|
+
dir.y *= distance
|
30
|
+
dir.z *= distance
|
31
|
+
|
32
|
+
edge.start_point.x += dir.x
|
33
|
+
edge.start_point.y += dir.y
|
34
|
+
edge.start_point.z += dir.z
|
35
|
+
edge.end_point.x += dir.x
|
36
|
+
edge.end_point.y += dir.y
|
37
|
+
edge.end_point.z += dir.z
|
38
|
+
|
39
|
+
edge
|
40
|
+
end
|
41
|
+
|
42
|
+
def extrude(distance,direction)
|
43
|
+
edge = clone
|
44
|
+
[edge.start_point,edge.end_point].each do |v|
|
45
|
+
v.x += distance*direction.x
|
46
|
+
v.y += distance*direction.y
|
47
|
+
v.z += distance*direction.z
|
48
|
+
end
|
49
|
+
|
50
|
+
poly = Polygon.new
|
51
|
+
poly.vertices.push(
|
52
|
+
edge.end_point , edge.start_point,
|
53
|
+
@start_point , @end_point)
|
54
|
+
poly
|
55
|
+
end
|
56
|
+
|
57
|
+
def snap(point)
|
58
|
+
x1 = @start_point.x
|
59
|
+
y1 = @start_point.y
|
60
|
+
z1 = @start_point.z
|
61
|
+
x2 = @end_point.x
|
62
|
+
y2 = @end_point.y
|
63
|
+
z2 = @end_point.z
|
64
|
+
x3 = point.x
|
65
|
+
y3 = point.y
|
66
|
+
z3 = point.z
|
67
|
+
dx = x2-x1
|
68
|
+
dy = y2-y1
|
69
|
+
dz = z2-z1
|
70
|
+
if dx == 0 && dy == 0 && dz == 0
|
71
|
+
return @start_point
|
72
|
+
else
|
73
|
+
t = ((x3 - x1) * dx + (y3 - y1) * dy + (z3 - z1) * dz) / (dx**2 + dy**2 + dz**2)
|
74
|
+
x0 = x1 + t * dx
|
75
|
+
y0 = y1 + t * dy
|
76
|
+
z0 = z1 + t * dz
|
77
|
+
return Vertex.new(x0,y0,z0)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def clone
|
82
|
+
Edge.new(@start_point.clone,@end_point.clone)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
"#<Geom::Edge:#{@start_point.to_s},#{@end_point.to_s}>"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|