fml 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/README +1 -0
- data/Rakefile +3 -0
- data/bin/fml2dae.rb +15 -0
- data/bin/fml2obj.rb +10 -0
- data/fml.gemspec +36 -0
- data/lib/collada/document.rb +101 -0
- data/lib/collada/geometry.rb +110 -0
- data/lib/config.yml +17 -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 +108 -0
- data/lib/floorplanner/document.rb +68 -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/floorplanner.rb +52 -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/glu_tess.rb +34 -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/geom.rb +13 -0
- data/lib/keyhole/archive.rb +36 -0
- data/tasks/github-gem.rake +315 -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
- data/xml/fml2kml.xsl +59 -0
- metadata +117 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
class Design
|
3
|
+
DESIGN_QUERY = "/design"
|
4
|
+
ASSET_QUERY = DESIGN_QUERY+"/assets/asset[@id='%s']"
|
5
|
+
ASSET_URL2D = ASSET_QUERY+"/url2d"
|
6
|
+
LINES_QUERY = DESIGN_QUERY+"/lines/line"
|
7
|
+
OPENINGS_QUERY = DESIGN_QUERY+"/objects/object[type='opening']"
|
8
|
+
AREAS_QUERY = DESIGN_QUERY+"/areas/area"
|
9
|
+
NAME_QUERY = DESIGN_QUERY+"/name"
|
10
|
+
|
11
|
+
include ColladaExport
|
12
|
+
include ObjExport
|
13
|
+
include RibExport
|
14
|
+
include SvgExport
|
15
|
+
|
16
|
+
##
|
17
|
+
# Constructs new floorplan design from FML
|
18
|
+
##
|
19
|
+
def initialize(fml)
|
20
|
+
begin
|
21
|
+
@xml = fml
|
22
|
+
@name = @xml.find(NAME_QUERY).first.content
|
23
|
+
@author = "John Doe" # TODO from <author> element if included in FML
|
24
|
+
rescue NoMethodError
|
25
|
+
Log.error "Can't load FML"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Builds geometries of walls and areas.
|
31
|
+
##
|
32
|
+
def build_geometries
|
33
|
+
@areas = AreaBuilder.new do |b|
|
34
|
+
@xml.find(AREAS_QUERY).each do |area|
|
35
|
+
name = area.find('name').first
|
36
|
+
name = name.content if name
|
37
|
+
color = area.find('color').first
|
38
|
+
color = color.content if color
|
39
|
+
type = area.find('type').first.content
|
40
|
+
|
41
|
+
asset_id = area.find('asset').first
|
42
|
+
asset_id = asset_id.attributes['refid'] if asset_id
|
43
|
+
if asset_id
|
44
|
+
texture_url = @xml.find(ASSET_URL2D % [asset_id]).first.content
|
45
|
+
end
|
46
|
+
|
47
|
+
vertices = Array.new
|
48
|
+
area.find('points').first.content.split(',').each do |str_v|
|
49
|
+
floats = str_v.strip.split(/\s/).map! {|f| f.to_f}
|
50
|
+
|
51
|
+
# TODO: fix y coords in Flash app
|
52
|
+
floats[1] *= -1.0; floats[4] *= -1.0
|
53
|
+
|
54
|
+
vertices << b.vertex(Geom::Vertex.new(*floats[0..2]))
|
55
|
+
vertices << b.vertex(Geom::Vertex.new(*floats[3..5]))
|
56
|
+
end
|
57
|
+
|
58
|
+
b.area(vertices,
|
59
|
+
:color => color,
|
60
|
+
:name => name,
|
61
|
+
:texture => texture_url,
|
62
|
+
:type => type)
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
min_height = 10
|
67
|
+
@walls = WallBuilder.new do |b|
|
68
|
+
@xml.find(LINES_QUERY).each do |line|
|
69
|
+
floats = line.find('points').first.get_floats
|
70
|
+
|
71
|
+
thickness = line.find('thickness').first
|
72
|
+
thickness = thickness ? thickness.content.to_f : Floorplanner.config['wall_thickness']
|
73
|
+
height = line.find('height').first
|
74
|
+
height = height ? height.content.to_f : Floorplanner.config['wall_height']
|
75
|
+
|
76
|
+
# TODO: fix this in Flash app
|
77
|
+
floats[1] *= -1.0; floats[4] *= -1.0
|
78
|
+
|
79
|
+
sp = Geom::Vertex.new(*floats[0..2])
|
80
|
+
ep = Geom::Vertex.new(*floats[3..5])
|
81
|
+
sp = b.vertex(sp)
|
82
|
+
ep = b.vertex(ep)
|
83
|
+
b.wall(sp,ep,thickness,height)
|
84
|
+
min_height = height if height < min_height
|
85
|
+
end
|
86
|
+
end
|
87
|
+
@areas.update min_height
|
88
|
+
|
89
|
+
@walls.prepare
|
90
|
+
@xml.find(OPENINGS_QUERY).each do |opening|
|
91
|
+
pos_floats = opening.find('points').first.get_floats
|
92
|
+
|
93
|
+
# TODO: fix y coord in Flash app
|
94
|
+
pos_floats[1] *= -1
|
95
|
+
|
96
|
+
size_floats = opening.find('size').first.get_floats
|
97
|
+
position = Geom::Number3D.new(*pos_floats)
|
98
|
+
size = Geom::Number3D.new(*size_floats)
|
99
|
+
|
100
|
+
asset_id = opening.find('asset').first.attributes['refid']
|
101
|
+
asset = @xml.find(ASSET_QUERY % [asset_id]).first
|
102
|
+
type = asset.find('url2d').first.content.match(/door/i) ? Opening3D::TYPE_DOOR : Opening3D::TYPE_WINDOW
|
103
|
+
@walls.opening(position,size,type)
|
104
|
+
end
|
105
|
+
@walls.update
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Floorplanner
|
2
|
+
|
3
|
+
class Document
|
4
|
+
|
5
|
+
POINTS_QUERY = "/project/floors/floor/designs/design/area/line/points"
|
6
|
+
LINE_POINTS_REGEXP = /^((\s*[-+]?[0-9]*\.?[0-9]+\s+){5,8}\s*[-+]?[0-9]*\.?[0-9]+\s*?(?:,)?)*$/
|
7
|
+
|
8
|
+
def initialize(fml_fn)
|
9
|
+
@xml = XML::Document.file(fml_fn)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.validate(doc)
|
13
|
+
schema = XML::Schema.document(
|
14
|
+
XML::Document.file(File.join(File.dirname(__FILE__), "..", "..", "xml", "fml-permissive.xsd"))
|
15
|
+
)
|
16
|
+
doc = XML::Document.file(doc) if doc.instance_of?(String)
|
17
|
+
doc.validate_schema(schema) do |message,error|
|
18
|
+
# TODO throw an exception
|
19
|
+
puts message if error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.validate_line_points(doc)
|
26
|
+
doc.find(POINTS_QUERY).each do |points_node|
|
27
|
+
unless LINE_POINTS_REGEXP =~ points_node.children.to_s
|
28
|
+
# TODO throw an exception
|
29
|
+
puts "Elements points inside area's line failed to validate content."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class DesignDocument
|
36
|
+
|
37
|
+
def initialize(fml_fn)
|
38
|
+
@xml = LibXML::XML::Document.file(fml_fn)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_heights(new_height)
|
42
|
+
lines = @xml.find("/design/lines/line[type='default_wall' or type='normal_wall' or contains(type,'hedge') or contains(type,'fence')]")
|
43
|
+
lines.each do |line|
|
44
|
+
begin
|
45
|
+
points = line.find("points").first
|
46
|
+
next unless points.content.include? ","
|
47
|
+
|
48
|
+
coords = points.content.strip.split(",")
|
49
|
+
top_coords = coords[1].strip.split(/\s/).map{|c| c.to_f}
|
50
|
+
|
51
|
+
top_coords[2] = new_height
|
52
|
+
top_coords[5] = new_height
|
53
|
+
if top_coords.length > 6
|
54
|
+
top_coords[8] = new_height
|
55
|
+
end
|
56
|
+
|
57
|
+
coords[1] = top_coords.join(" ")
|
58
|
+
points.content = coords.join(",")
|
59
|
+
rescue; end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def save(fn)
|
64
|
+
@xml.save fn
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
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
|
@@ -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
|
+
Log.info "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
|
+
Log.info "Walls Vertices: #{@vertices.length.to_s}"
|
124
|
+
Log.info "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/floorplanner.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'yaml'
|
3
|
+
require 'find'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'zip/zip'
|
11
|
+
require 'xml'
|
12
|
+
|
13
|
+
$LOAD_PATH.push(File.dirname(__FILE__))
|
14
|
+
|
15
|
+
module Floorplanner
|
16
|
+
|
17
|
+
Log = Logger.new STDOUT
|
18
|
+
Log.level = Logger::WARN
|
19
|
+
Log.datetime_format = "%Y-%m-%d %H:%M:%S "
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
@@config ||= YAML.load_file(File.join(File.dirname(__FILE__),'config.yml'))
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.config=(yaml)
|
26
|
+
@@config = yaml
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module XML
|
31
|
+
class Node
|
32
|
+
def get_floats
|
33
|
+
content.split(/\s/).delete_if {|s| s.empty?}.map! {|f| f.to_f}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'geom'
|
39
|
+
require 'floorplanner/asset'
|
40
|
+
require 'floorplanner/document'
|
41
|
+
require 'floorplanner/collada_export'
|
42
|
+
require 'floorplanner/rib_export'
|
43
|
+
require 'floorplanner/obj_export'
|
44
|
+
require 'floorplanner/svg_export'
|
45
|
+
require 'floorplanner/design'
|
46
|
+
require 'floorplanner/wall3d'
|
47
|
+
require 'floorplanner/opening3d'
|
48
|
+
require 'floorplanner/wall_builder'
|
49
|
+
require 'floorplanner/area_builder'
|
50
|
+
require 'collada/document'
|
51
|
+
require 'collada/geometry'
|
52
|
+
require 'keyhole/archive'
|