fml 0.2.1
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/.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'
|