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.
@@ -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