floorplanner-fml 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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