floorplanner-fml 0.2
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/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
|