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,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
|