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,118 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Document
|
3
|
+
|
4
|
+
def to_dae(design_id,out_path,xrefs=false)
|
5
|
+
@design = Design.new(@xml,design_id)
|
6
|
+
@design.build_geometries
|
7
|
+
@design.save_textures File.dirname(out_path) unless xrefs
|
8
|
+
dae = File.new(out_path,'w')
|
9
|
+
dae.write @design.to_dae xrefs
|
10
|
+
dae.close
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
module ColladaExport
|
16
|
+
DESIGN_QUERY = "/project/floors/floor/designs/design[id='%s']"
|
17
|
+
ASSET_QUERY = DESIGN_QUERY+"/assets/asset[@id='%s']"
|
18
|
+
ASSETS_QUERY = DESIGN_QUERY+"/assets/asset"
|
19
|
+
OBJECTS_QUERY = DESIGN_QUERY+"/objects/object"
|
20
|
+
|
21
|
+
def to_dae(xrefs=false)
|
22
|
+
raise "No geometries to export. Call build_geometries first" unless @areas && @walls
|
23
|
+
@assets = assets
|
24
|
+
@elements = objects
|
25
|
+
|
26
|
+
# somehow...
|
27
|
+
@walls.reverse
|
28
|
+
@areas.each {|a| a.reverse}
|
29
|
+
@xrefs = xrefs
|
30
|
+
|
31
|
+
template = ERB.new(
|
32
|
+
File.read(
|
33
|
+
File.join(File.dirname(__FILE__), '..', '..', 'views', 'design.dae.erb')))
|
34
|
+
template.result(binding)
|
35
|
+
end
|
36
|
+
|
37
|
+
def assets
|
38
|
+
return @assets if @assets
|
39
|
+
@assets = {}
|
40
|
+
@xml.find(ASSETS_QUERY % @design_id).each do |asset_node|
|
41
|
+
asset_id = asset_node.attributes['id']
|
42
|
+
name = asset_node.find('name').first.content
|
43
|
+
url3d = asset_node.find('url3d').first
|
44
|
+
next unless url3d
|
45
|
+
url3d = url3d.content
|
46
|
+
|
47
|
+
# TODO: store asset bounding box
|
48
|
+
asset = Floorplanner::Asset.get(asset_id,name,url3d)
|
49
|
+
next unless asset
|
50
|
+
@assets.store(asset_id, asset)
|
51
|
+
end
|
52
|
+
@assets
|
53
|
+
end
|
54
|
+
|
55
|
+
def objects
|
56
|
+
result = []
|
57
|
+
@xml.find(OBJECTS_QUERY % @design_id).each do |object|
|
58
|
+
begin
|
59
|
+
refid = object.find('asset').first.attributes['refid']
|
60
|
+
next unless assets[refid]
|
61
|
+
|
62
|
+
asset = assets[refid]
|
63
|
+
position = Geom::Number3D.from_str(object.find('points').first.content)
|
64
|
+
# correct Flash axis issues
|
65
|
+
position.y *= -1.0
|
66
|
+
|
67
|
+
# correct Flash rotation issues
|
68
|
+
rotation = unless object.find('rotation').empty?
|
69
|
+
object.find('rotation').first.content
|
70
|
+
else
|
71
|
+
'0 0 0'
|
72
|
+
end
|
73
|
+
rotation = Geom::Number3D.from_str(rotation)
|
74
|
+
rotation.z += 360 if rotation.z < 0
|
75
|
+
rotation.z += 180
|
76
|
+
|
77
|
+
# find proper scale for object
|
78
|
+
size = object.find('size').first.content
|
79
|
+
scale = asset.scale_ratio(Geom::Number3D.from_str(size))
|
80
|
+
|
81
|
+
mirrored = object.find('mirrored').first
|
82
|
+
reflection = Geom::Matrix3D.reflection(Geom::Plane.new(Geom::Number3D.new(0.0,1.0,0.0), Geom::Number3D.new))
|
83
|
+
if mirrored
|
84
|
+
mirror = Geom::Number3D.from_str(mirrored)
|
85
|
+
if mirror.x != 0 || mirror.y != 0 || mirror.z != 0
|
86
|
+
mirror.x = mirror.x > 0 ? 1 : 0
|
87
|
+
mirror.y = mirror.y > 0 ? 1 : 0
|
88
|
+
mirror.z = mirror.z > 0 ? 1 : 0
|
89
|
+
|
90
|
+
origin = Geom::Number3D.new
|
91
|
+
plane = Geom::Plane.new(mirror,origin)
|
92
|
+
reflection = Geom::Matrix3D.reflection(plane).multiply reflection
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
result << {
|
97
|
+
:asset => asset,
|
98
|
+
:position => position,
|
99
|
+
:rotation => rotation,
|
100
|
+
:scale => scale,
|
101
|
+
:matrix => reflection
|
102
|
+
}
|
103
|
+
rescue
|
104
|
+
# TODO: handle text
|
105
|
+
end
|
106
|
+
end
|
107
|
+
result
|
108
|
+
end
|
109
|
+
|
110
|
+
def save_textures(root_path)
|
111
|
+
img_path = File.join(root_path,'textures')
|
112
|
+
FileUtils.mkdir_p img_path
|
113
|
+
assets.each_value do |asset|
|
114
|
+
asset.save_textures img_path
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Design
|
3
|
+
DESIGN_QUERY = "/project/floors/floor/designs/design[id='%s']"
|
4
|
+
DESIGN_N_QUERY = "/project/floors/floor/designs/design[name='%s']"
|
5
|
+
ASSET_QUERY = DESIGN_QUERY+"/assets/asset[@id='%s']"
|
6
|
+
ASSET_URL2D = ASSET_QUERY+"/url2d"
|
7
|
+
LINES_QUERY = DESIGN_QUERY+"/lines/line"
|
8
|
+
OPENINGS_QUERY = DESIGN_QUERY+"/objects/object[type='opening']"
|
9
|
+
AREAS_QUERY = DESIGN_QUERY+"/areas/area"
|
10
|
+
NAME_QUERY = DESIGN_QUERY+"/name"
|
11
|
+
|
12
|
+
include ColladaExport
|
13
|
+
include ObjExport
|
14
|
+
include RibExport
|
15
|
+
include SvgExport
|
16
|
+
|
17
|
+
##
|
18
|
+
# Constructs new floorplan design from FML
|
19
|
+
##
|
20
|
+
def initialize(fml,design_id)
|
21
|
+
begin
|
22
|
+
@xml = fml
|
23
|
+
unless fml.find(DESIGN_QUERY % design_id).length.zero?
|
24
|
+
@design_id = design_id
|
25
|
+
else
|
26
|
+
@design_id = fml.find(DESIGN_N_QUERY % design_id).first.find("id").first.content
|
27
|
+
end
|
28
|
+
@name = @xml.find(NAME_QUERY % @design_id).first.content
|
29
|
+
@author = "John Doe" # TODO from <author> element if included in FML
|
30
|
+
rescue NoMethodError
|
31
|
+
$stderr.puts "Can't find Design with ID or name: %s" % design_id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Builds geometries of walls and areas.
|
37
|
+
##
|
38
|
+
def build_geometries
|
39
|
+
@areas = AreaBuilder.new do |b|
|
40
|
+
@xml.find(AREAS_QUERY % @design_id).each do |area|
|
41
|
+
name = area.find('name').first.content
|
42
|
+
color = area.find('color').first.content
|
43
|
+
type = area.find('type').first.content
|
44
|
+
|
45
|
+
asset_id = area.find('asset').first.attributes['refid']
|
46
|
+
texture_url = @xml.find(ASSET_URL2D % [@design_id,asset_id]).first.content
|
47
|
+
|
48
|
+
vertices = Array.new
|
49
|
+
area.find('points').first.content.split(',').each do |str_v|
|
50
|
+
floats = str_v.strip.split(/\s/).map! {|f| f.to_f}
|
51
|
+
|
52
|
+
# TODO: fix y coords in Flash app
|
53
|
+
floats[1] *= -1.0; floats[4] *= -1.0
|
54
|
+
|
55
|
+
vertices << b.vertex(Geom::Vertex.new(*floats[0..2]))
|
56
|
+
vertices << b.vertex(Geom::Vertex.new(*floats[3..5]))
|
57
|
+
end
|
58
|
+
|
59
|
+
b.area(vertices,
|
60
|
+
:color => color,
|
61
|
+
:name => name,
|
62
|
+
:texture => texture_url,
|
63
|
+
:type => type)
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
min_height = 10
|
68
|
+
@walls = WallBuilder.new do |b|
|
69
|
+
@xml.find(LINES_QUERY % @design_id).each do |line|
|
70
|
+
floats = line.find('points').first.get_floats
|
71
|
+
|
72
|
+
thickness = line.find('thickness').first.content.to_f
|
73
|
+
height = line.find('height').first.content.to_f
|
74
|
+
|
75
|
+
# TODO: fix this in Flash app
|
76
|
+
floats[1] *= -1.0; floats[4] *= -1.0
|
77
|
+
|
78
|
+
sp = Geom::Vertex.new(*floats[0..2])
|
79
|
+
ep = Geom::Vertex.new(*floats[3..5])
|
80
|
+
sp = b.vertex(sp)
|
81
|
+
ep = b.vertex(ep)
|
82
|
+
b.wall(sp,ep,thickness,height)
|
83
|
+
min_height = height if height < min_height
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@areas.update min_height
|
87
|
+
|
88
|
+
@walls.prepare
|
89
|
+
@xml.find(OPENINGS_QUERY % @design_id).each do |opening|
|
90
|
+
pos_floats = opening.find('points').first.get_floats
|
91
|
+
|
92
|
+
# TODO: fix y coord in Flash app
|
93
|
+
pos_floats[1] *= -1
|
94
|
+
|
95
|
+
size_floats = opening.find('size').first.get_floats
|
96
|
+
position = Geom::Number3D.new(*pos_floats)
|
97
|
+
size = Geom::Number3D.new(*size_floats)
|
98
|
+
|
99
|
+
asset_id = opening.find('asset').first.attributes['refid']
|
100
|
+
asset = @xml.find(ASSET_QUERY % [@design_id,asset_id]).first
|
101
|
+
type = asset.find('url2d').first.content.match(/door/i) ? Opening3D::TYPE_DOOR : Opening3D::TYPE_WINDOW
|
102
|
+
@walls.opening(position,size,type)
|
103
|
+
end
|
104
|
+
@walls.update
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
|
3
|
+
class Document
|
4
|
+
POINTS_QUERY = "/project/floors/floor/designs/design/area/line/points"
|
5
|
+
LINE_POINTS_REGEXP = /^((\s*[-+]?[0-9]*\.?[0-9]+\s+){5,8}\s*[-+]?[0-9]*\.?[0-9]+\s*?(?:,)?)*$/
|
6
|
+
|
7
|
+
def initialize(fml_fn)
|
8
|
+
@xml = XML::Document.file(fml_fn)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.validate(doc)
|
12
|
+
schema = XML::RelaxNG.document(
|
13
|
+
XML::Document.file(File.join(File.dirname(__FILE__), "..", "..", "xml", "fml.rng"))
|
14
|
+
)
|
15
|
+
doc = XML::Document.file(doc) if doc.instance_of?(String)
|
16
|
+
doc.validate_relaxng(schema) do |message,error|
|
17
|
+
# TODO throw an exception
|
18
|
+
puts message if error
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.validate_line_points(doc)
|
25
|
+
doc.find(POINTS_QUERY).each do |points_node|
|
26
|
+
unless LINE_POINTS_REGEXP =~ points_node.children.to_s
|
27
|
+
# TODO throw an exception
|
28
|
+
puts "Elements points inside area's line failed to validate content."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class DesignDocument
|
35
|
+
def initialize(fml_fn)
|
36
|
+
@xml = XML::Document.file(fml_fn)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_heights(new_height)
|
40
|
+
lines = @xml.find("/design/lines/line[type='default_wall' or type='normal_wall' or contains(type,'hedge') or contains(type,'fence')]")
|
41
|
+
lines.each do |line|
|
42
|
+
begin
|
43
|
+
points = line.find("points").first
|
44
|
+
next unless points.content.include? ","
|
45
|
+
|
46
|
+
coords = points.content.strip.split(",")
|
47
|
+
top_coords = coords[1].strip.split(/\s/).map{|c| c.to_f}
|
48
|
+
|
49
|
+
top_coords[2] = new_height
|
50
|
+
top_coords[5] = new_height
|
51
|
+
if top_coords.length > 6
|
52
|
+
top_coords[8] = new_height
|
53
|
+
end
|
54
|
+
|
55
|
+
coords[1] = top_coords.join(" ")
|
56
|
+
points.content = coords.join(",")
|
57
|
+
rescue; end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def save(fn)
|
62
|
+
@xml.save fn
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Document
|
3
|
+
|
4
|
+
def to_obj(design_id,out_path)
|
5
|
+
@design = Design.new(@xml,design_id)
|
6
|
+
@design.build_geometries
|
7
|
+
obj = File.new(out_path,'w')
|
8
|
+
obj.write @design.to_obj
|
9
|
+
obj.close
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
module ObjExport
|
15
|
+
def to_obj
|
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.obj.erb')))
|
21
|
+
template.result(binding)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Opening3D < Geom::TriangleMesh
|
3
|
+
|
4
|
+
TYPE_DOOR = 1
|
5
|
+
TYPE_WINDOW = 2
|
6
|
+
|
7
|
+
attr_accessor(:position,:window)
|
8
|
+
|
9
|
+
def initialize(baseline,thickness,opening)
|
10
|
+
super()
|
11
|
+
@position = baseline.snap(opening[:position])
|
12
|
+
@type = opening[:type]
|
13
|
+
dir = baseline.direction
|
14
|
+
angle = Math.atan2(dir.y,dir.x)
|
15
|
+
width = opening[:size].x
|
16
|
+
height = 0
|
17
|
+
|
18
|
+
case @type
|
19
|
+
when TYPE_DOOR
|
20
|
+
@position.z = 0.01
|
21
|
+
height = Floorplanner.config['openings']['door_height']
|
22
|
+
else
|
23
|
+
@position.z = Floorplanner.config['openings']['window_base']
|
24
|
+
height = Floorplanner.config['openings']['window_height']
|
25
|
+
end
|
26
|
+
|
27
|
+
v1 = Geom::Vertex.new(-width/2,0,0)
|
28
|
+
v2 = Geom::Vertex.new( width/2,0,0)
|
29
|
+
o_base = Geom::Edge.new(v1,v2)
|
30
|
+
|
31
|
+
# create opening side
|
32
|
+
o_inner = o_base.offset(thickness/2.0,Wall3D::UP)
|
33
|
+
o_outer = o_base.offset(-thickness/2.0,Wall3D::UP)
|
34
|
+
|
35
|
+
@base = Geom::Polygon.new([
|
36
|
+
o_inner.end_point, o_inner.start_point,
|
37
|
+
o_outer.start_point , o_outer.end_point
|
38
|
+
])
|
39
|
+
|
40
|
+
# rotate in wall's direction
|
41
|
+
@base.transform_vertices(Geom::Matrix3D.rotationZ(angle))
|
42
|
+
# move to position
|
43
|
+
@base.transform_vertices(Geom::Matrix3D.translation(@position.x,@position.y,@position.z))
|
44
|
+
|
45
|
+
extrusion = @base.extrude(height,Wall3D::UP,nil,false)
|
46
|
+
|
47
|
+
# delete sides
|
48
|
+
extrusion.delete_at(0)
|
49
|
+
extrusion.delete_at(1)
|
50
|
+
|
51
|
+
# flip top cap
|
52
|
+
extrusion.last.reverse
|
53
|
+
|
54
|
+
@meshes << @base
|
55
|
+
@meshes.concat(extrusion)
|
56
|
+
|
57
|
+
# create glass
|
58
|
+
if @type == TYPE_WINDOW
|
59
|
+
g_inner = o_base.offset( 0.02, Wall3D::UP)
|
60
|
+
g_outer = o_base.offset(-0.02, Wall3D::UP)
|
61
|
+
|
62
|
+
glass_base = Geom::Polygon.new([
|
63
|
+
g_inner.end_point, g_inner.start_point,
|
64
|
+
g_outer.start_point , g_outer.end_point
|
65
|
+
])
|
66
|
+
|
67
|
+
# rotate in wall's direction
|
68
|
+
glass_base.transform_vertices(Geom::Matrix3D.rotationZ(angle))
|
69
|
+
# move to position
|
70
|
+
glass_base.transform_vertices(Geom::Matrix3D.translation(@position.x,@position.y,@position.z))
|
71
|
+
|
72
|
+
extrusion = glass_base.extrude(height,Wall3D::UP,nil,false)
|
73
|
+
|
74
|
+
# flip base cap
|
75
|
+
glass_base.reverse
|
76
|
+
|
77
|
+
@window = Geom::TriangleMesh.new
|
78
|
+
@window.meshes.concat extrusion
|
79
|
+
@window << glass_base
|
80
|
+
@window.update
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# drill hole to sides
|
85
|
+
def drill(mesh,outer)
|
86
|
+
side = outer ? mesh.meshes.first : mesh.meshes.last
|
87
|
+
|
88
|
+
# opening start
|
89
|
+
t1 = @meshes.first.vertices[outer ? 0 : 3].clone
|
90
|
+
t1.z = side.vertices[0].z
|
91
|
+
t1b = @meshes[3].vertices[outer ? 0 : 3]
|
92
|
+
|
93
|
+
b1 = @meshes.first.vertices[outer ? 0 : 3].clone
|
94
|
+
b1.z = side.vertices[2].z
|
95
|
+
b1t = @meshes.first.vertices[outer ? 0 : 3]
|
96
|
+
|
97
|
+
# opening end
|
98
|
+
t2 = @meshes.first.vertices[outer ? 1 : 2].clone
|
99
|
+
t2.z = side.vertices[0].z
|
100
|
+
t2b = @meshes[3].vertices[outer ? 1 : 2]
|
101
|
+
|
102
|
+
b2 = @meshes.first.vertices[outer ? 1 : 2].clone
|
103
|
+
b2.z = side.vertices[2].z
|
104
|
+
b2t = @meshes.first.vertices[outer ? 1 : 2]
|
105
|
+
|
106
|
+
# old side vertices
|
107
|
+
ot = side.vertices[1]
|
108
|
+
ob = side.vertices[2]
|
109
|
+
side.vertices[1] = outer ? t2 : t1
|
110
|
+
side.vertices[2] = outer ? b2 : b1
|
111
|
+
|
112
|
+
# polygon above opening
|
113
|
+
op_top = Geom::Polygon.new
|
114
|
+
if outer
|
115
|
+
op_top.vertices.push(t2,t1,t1b,t2b)
|
116
|
+
else
|
117
|
+
op_top.vertices.push(t1,t2,t2b,t1b)
|
118
|
+
end
|
119
|
+
|
120
|
+
# polygon below opening
|
121
|
+
op_bot = Geom::Polygon.new
|
122
|
+
if outer
|
123
|
+
op_bot.vertices.push(b2t,b1t,b1,b2)
|
124
|
+
else
|
125
|
+
op_bot.vertices.push(b1t,b2t,b2,b1)
|
126
|
+
end
|
127
|
+
|
128
|
+
rest = Geom::Polygon.new
|
129
|
+
if outer
|
130
|
+
rest.vertices.push(t1,ot,ob,b1)
|
131
|
+
else
|
132
|
+
rest.vertices.push(t2,ot,ob,b2)
|
133
|
+
end
|
134
|
+
|
135
|
+
mesh.meshes.push(op_top)
|
136
|
+
mesh.meshes.push(op_bot)
|
137
|
+
mesh.meshes.push(rest)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|