fml 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,35 +1,14 @@
1
1
  module Floorplanner
2
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
3
 
12
4
  include ColladaExport
13
- include ObjExport
14
- include RibExport
15
5
  include SvgExport
16
6
 
17
7
  ##
18
8
  # Constructs new floorplan design from FML
19
9
  ##
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
10
+ def initialize(doc)
11
+ @doc = doc
33
12
  end
34
13
 
35
14
  ##
@@ -37,71 +16,58 @@ module Floorplanner
37
16
  ##
38
17
  def build_geometries
39
18
  @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]))
19
+ @doc.areas.each do |area|
20
+ if area.asset
21
+ texture_url = @doc.asset(area.asset).url2d
57
22
  end
58
23
 
59
- b.area(vertices,
60
- :color => color,
61
- :name => name,
24
+ b.area(area.vertices,
25
+ :color => area.color,
26
+ :name => area.name,
62
27
  :texture => texture_url,
63
- :type => type)
64
-
28
+ :type => area.type)
65
29
  end
66
30
  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
31
 
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
32
+ heights = Hash.new
33
+ @walls = WallBuilder.new do |b|
34
+ @doc.lines.select{|l| l.type == :default_wall}.each do |line|
35
+ sp = b.vertex(line.vertices[0])
36
+ ep = b.vertex(line.vertices[1])
37
+ b.wall(sp, ep, line.thickness, line.height)
38
+
39
+ if heights.include?(line.height)
40
+ heights[line.height] += 1
41
+ else
42
+ heights[line.height] = 1
43
+ end
84
44
  end
85
45
  end
86
- @areas.update min_height
46
+ # get the most used (common) height accross linears
47
+ @areas.update(heights.sort{|a,b| a[1]<=>b[1]}.last[0]-0.02)
87
48
 
88
49
  @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)
50
+ @doc.openings.each do |opening|
51
+ asset = @doc.asset(opening.asset)
52
+ type = which_opening(opening, asset)
53
+ @walls.opening(opening.position ,opening.size, type)
103
54
  end
104
55
  @walls.update
105
56
  end
57
+
58
+ private
59
+
60
+ def which_opening(opening, asset)
61
+ case opening.type
62
+ when :door
63
+ type = Opening3D::TYPE_DOOR
64
+ when :window
65
+ type = Opening3D::TYPE_WINDOW
66
+ else
67
+ type = asset.url2d.match(/door/i) ?
68
+ Opening3D::TYPE_DOOR : Opening3D::TYPE_WINDOW
69
+ end
70
+ type
71
+ end
106
72
  end
107
73
  end
@@ -1,59 +1,33 @@
1
1
  module Floorplanner
2
2
 
3
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 = LibXML::XML::Document.file(fml_fn)
9
- end
10
-
11
- def self.validate(doc)
12
- schema = LibXML::XML::RelaxNG.document(
13
- LibXML::XML::Document.file(File.join(File.dirname(__FILE__), "..", "..", "xml", "fml.rng"))
14
- )
15
- doc = LibXML::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
4
+ def initialize fml_fn
5
+ @xml = Nokogiri::XML.parse(open(fml_fn))
31
6
  end
32
7
  end
33
8
 
34
9
  class DesignDocument
35
-
36
- def initialize(fml)
10
+ def initialize fml
37
11
  if fml.kind_of? String # filename
38
- @xml = LibXML::XML::Document.file(fml)
39
- elsif fml.kind_of? LibXML::XML::Document
12
+ @xml = Nokogiri::XML.parse(fml)
13
+ elsif fml.kind_of? Nokogiri::XML::Document
40
14
  @xml = fml
41
15
  elsif fml.respond_to?(:read) # IO
42
- @xml = LibXML::XML::Document.io(fml)
16
+ @xml = Nokogiri::XML.parse(fml)
43
17
  else
44
18
  raise ArgumentError.new("values must be one of: filename, IO, LibXML::XML::Document")
45
19
  end
46
20
  end
47
21
 
48
- def update_heights(new_height)
49
- lines = @xml.find("/design/lines/line[type='default_wall' or type='normal_wall' or contains(type,'hedge') or contains(type,'fence')]")
22
+ def update_heights new_height
23
+ lines = @xml.xpath("/design/lines/line[type='default_wall' or type='normal_wall' or contains(type,'hedge') or contains(type,'fence')]")
50
24
  lines.each do |line|
51
25
  begin
52
- points = line.find("points").first
26
+ points = line.xpath("points").first
53
27
  next unless points.content.include? ","
54
28
 
55
29
  coords = points.content.strip.split(",")
56
- top_coords = coords[1].strip.split(/\s/).map{|c| c.to_f}
30
+ top_coords = coords[1].strip.split(/\s/).map(&:to_f)
57
31
 
58
32
  top_coords[2] = new_height
59
33
  top_coords[5] = new_height
@@ -67,11 +41,11 @@ module Floorplanner
67
41
  end
68
42
  end
69
43
 
70
- def update_thumb_2d_url(thumb_2d_url)
71
- if thumb_node = @xml.find_first('/design/thumb-2d-url')
44
+ def update_thumb_2d_url thumb_2d_url
45
+ if thumb_node = @xml.xpath('/design/thumb-2d-url').first
72
46
  thumb_node.content = thumb_2d_url
73
- elsif design_node = @xml.find_first('/design')
74
- thumb_node = LibXML::XML::Node.new('thumb-2d-url')
47
+ elsif design_node = @xml.xpath('/design').first
48
+ thumb_node = @xml.create_element('thumb-2d-url')
75
49
  thumb_node.content = thumb_2d_url
76
50
  design_node << thumb_node
77
51
  else
@@ -79,8 +53,8 @@ module Floorplanner
79
53
  end
80
54
  end
81
55
 
82
- def save(path)
83
- @xml.save path
56
+ def save path
57
+ @xml.write_to open(path, 'w')
84
58
  end
85
59
 
86
60
  def to_xml
@@ -91,5 +65,4 @@ module Floorplanner
91
65
  @xml.to_s
92
66
  end
93
67
  end
94
-
95
68
  end
@@ -8,20 +8,21 @@ module Floorplanner
8
8
 
9
9
  def initialize(baseline,thickness,opening)
10
10
  super()
11
+ base_z = opening[:position].z
11
12
  @position = baseline.snap(opening[:position])
12
13
  @type = opening[:type]
14
+ @size = opening[:size]
13
15
  dir = baseline.direction
14
16
  angle = Math.atan2(dir.y,dir.x)
15
17
  width = opening[:size].x
16
- height = 0
17
18
 
18
19
  case @type
19
20
  when TYPE_DOOR
20
21
  @position.z = 0.01
21
- height = Floorplanner.config['openings']['door_height']
22
+ height = @size.z == 0 ? Floorplanner.config['openings']['door_height'] : @size.z
22
23
  else
23
- @position.z = Floorplanner.config['openings']['window_base']
24
- height = Floorplanner.config['openings']['window_height']
24
+ @position.z = base_z == 0 ? Floorplanner.config['openings']['window_base'] : base_z
25
+ height = @size.z == 0 ? Floorplanner.config['openings']['window_height'] : @size.z
25
26
  end
26
27
 
27
28
  v1 = Geom::Vertex.new(-width/2,0,0)
@@ -26,7 +26,7 @@ module Floorplanner
26
26
  end
27
27
  end
28
28
 
29
- def wall(sp,ep,thickness,height)
29
+ def wall(sp, ep, thickness, height)
30
30
  @connections[sp] = Array.new unless @connections.include?(sp)
31
31
  @connections[ep] = Array.new unless @connections.include?(ep)
32
32
  cs = Geom::Connection.new(ep, 0.0)
@@ -37,10 +37,11 @@ module Floorplanner
37
37
  end
38
38
 
39
39
  # call after adding walls
40
- def opening(position,size,type)
40
+ def opening(position, size, type)
41
41
  @walls.each do |wall|
42
42
  if wall.outline.point_inside(position)
43
43
  wall.opening(position,size,type)
44
+ break
44
45
  end
45
46
  end
46
47
  end
@@ -51,9 +52,9 @@ module Floorplanner
51
52
  next if connections.length.zero?
52
53
 
53
54
  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)
55
+ begin
56
+ c.angle = Math.atan2(c.point.x - v.x, c.point.y - v.y)
57
+ rescue; end
57
58
  end
58
59
  connections.sort! {|a,b| a.angle <=> b.angle}
59
60
  connections.each_index do |i|
@@ -0,0 +1,151 @@
1
+ module Floorplanner::XML
2
+
3
+ class Asset
4
+ include ROXML
5
+
6
+ xml_accessor :id, :from => :attr
7
+ xml_accessor :name
8
+ xml_accessor :url2d_str, :from => 'url2d'
9
+ xml_accessor :url3d
10
+
11
+ def url2d
12
+ url2d_str.match(/swf$/) ? url2d_str.gsub(/swf$/, 'png') : url2d_str
13
+ end
14
+ end
15
+
16
+ class Line
17
+ include ROXML
18
+
19
+ DEFAULT_HEIGHT = 2.4
20
+
21
+ xml_accessor :points
22
+ xml_accessor :type_str, :from => 'type'
23
+ xml_accessor :asset, :from => 'asset/@refid'
24
+ xml_accessor :thickness_str, :from => 'thickness'
25
+ xml_accessor :height_str, :from => 'height'
26
+
27
+ def type
28
+ type_str.to_sym
29
+ end
30
+
31
+ def height
32
+ height_str ? height_str.to_f : (
33
+ vertices[3] ? vertices[3].z : DEFAULT_HEIGHT)
34
+ end
35
+
36
+ def thickness
37
+ thickness_str.to_f
38
+ end
39
+
40
+ def vertices
41
+ @vertices ||= Floorplanner::XML.parse_points(points)
42
+ end
43
+ end
44
+
45
+ class Area
46
+ include ROXML
47
+
48
+ xml_accessor :points
49
+ xml_accessor :color
50
+ xml_accessor :name
51
+ xml_accessor :asset, :from => 'asset/@refid'
52
+ xml_accessor :type_str, :from => 'type'
53
+
54
+ def type
55
+ type_str.to_sym if type_str
56
+ end
57
+
58
+ def vertices
59
+ @vertices ||= Floorplanner::XML.parse_points(points)
60
+ end
61
+ end
62
+
63
+ class Item
64
+ include ROXML
65
+
66
+ xml_accessor :asset, :from => 'asset/@refid'
67
+ xml_accessor :type_str, :from => 'type'
68
+ xml_accessor :color
69
+
70
+ xml_accessor :points
71
+ xml_accessor :size_str, :from => 'size'
72
+ xml_accessor :rotation_str, :from => 'rotation'
73
+ xml_accessor :mirrored_str, :from => 'mirrored'
74
+
75
+ def type
76
+ type_str.to_sym
77
+ end
78
+
79
+ def size
80
+ @scale ||= Floorplanner::XML.parse_vector(size_str)
81
+ end
82
+
83
+ def position
84
+ @position ||= Floorplanner::XML.parse_point(points)
85
+ end
86
+
87
+ def rotation
88
+ @rotation ||= Floorplanner::XML.parse_rotation(rotation_str)
89
+ end
90
+
91
+ def mirrored
92
+ @mirrored ||= Floorplanner::XML.parse_vector(mirrored_str)
93
+ end
94
+ end
95
+
96
+ class Document
97
+ include ROXML
98
+
99
+ xml_accessor :name
100
+ xml_accessor :assets, :as => [Asset]
101
+ xml_accessor :lines, :as => [Line]
102
+ xml_accessor :areas, :as => [Area]
103
+ xml_accessor :objects, :as => [Item]
104
+ xml_accessor :openings, :as => [Item],
105
+ :from => "objects/object[(type='opening') or (type='window') or (type='door')]"
106
+
107
+ def asset(refid)
108
+ assets.detect { |a| a.id == refid }
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ # We are inverting Y coordinate here because of Flash
115
+ # inverted coordinate system
116
+
117
+ def self.parse_point(val)
118
+ f = val.split(/\s/).map{ |s| s.to_f }
119
+ Geom::Vertex.new(f[0], -f[1], f[2])
120
+ rescue
121
+ nil
122
+ end
123
+
124
+ # Inverting Z rotation (Flash again)
125
+
126
+ def self.parse_rotation(val)
127
+ f = val.split(/\s/).map{ |s| s.to_f }
128
+ Geom::Vertex.new(f[0], f[1], -f[2])
129
+ rescue
130
+ nil
131
+ end
132
+
133
+ def self.parse_vector(val)
134
+ Geom::Vertex.new(*val.split(/\s/).map{ |s| s.to_f })
135
+ rescue
136
+ nil
137
+ end
138
+
139
+ def self.parse_points(val)
140
+ points = val.split(/\s|,/)
141
+ raise "Invalid coords. count for points" unless points.length % 3 == 0
142
+
143
+ result = []
144
+ points.each_with_index do |x,i|
145
+ result << [] if i % 3 == 0
146
+ result.last << x.to_f
147
+ end
148
+ result.map { |f| Geom::Vertex.new(f[0], -f[1], f[2]) }
149
+ end
150
+ end
151
+
data/lib/floorplanner.rb CHANGED
@@ -7,7 +7,8 @@ require 'fileutils'
7
7
 
8
8
  require 'rubygems'
9
9
  require 'zip/zip'
10
- require 'xml'
10
+ require 'nokogiri'
11
+ require 'roxml'
11
12
 
12
13
  $LOAD_PATH.push(File.dirname(__FILE__))
13
14
 
@@ -16,25 +17,26 @@ module Floorplanner
16
17
  @@config ||= YAML.load_file(File.join(File.dirname(__FILE__),'config.yml'))
17
18
  end
18
19
 
19
- def self.config=(yaml)
20
+ def self.config= yaml
20
21
  @@config = yaml
21
22
  end
22
- end
23
23
 
24
- module XML
25
- class Node
26
- def get_floats
27
- content.split(/\s/).delete_if {|s| s.empty?}.map! {|f| f.to_f}
24
+ HEX_RE = "(?i:[a-f\\d])"
25
+
26
+ def self.read_color hexstring
27
+ if hexstring =~ /\A#((?:#{HEX_RE}{2,2}){3,4})\z/
28
+ return [*$1.scan(/.{2,2}/).collect {|value| value.hex / 255.0}]
29
+ else
30
+ return [1,1,1]
28
31
  end
29
32
  end
30
33
  end
31
34
 
32
35
  require 'geom'
36
+ require 'floorplanner/xml'
33
37
  require 'floorplanner/asset'
34
38
  require 'floorplanner/document'
35
39
  require 'floorplanner/collada_export'
36
- require 'floorplanner/rib_export'
37
- require 'floorplanner/obj_export'
38
40
  require 'floorplanner/svg_export'
39
41
  require 'floorplanner/design'
40
42
  require 'floorplanner/wall3d'
data/lib/geom/edge.rb CHANGED
@@ -22,7 +22,7 @@ module Geom
22
22
  edge = clone
23
23
  dir = direction
24
24
 
25
- Matrix3D.multiply_vector_3x3(Matrix3D.rotation_matrix(up.x,up.y,up.z, -Math::PI/2),dir)
25
+ Matrix3D.multiply_vector_3x3(Matrix3D.rotation(up.x,up.y,up.z, -Math::PI/2),dir)
26
26
  dir.normalize
27
27
 
28
28
  dir.x *= distance
data/lib/geom/matrix3d.rb CHANGED
@@ -9,7 +9,7 @@ module Geom
9
9
  ]
10
10
  end
11
11
 
12
- def self.rotation_matrix(x,y,z,rad)
12
+ def self.rotation(x,y,z,rad)
13
13
  n_cos = Math.cos(rad)
14
14
  n_sin = Math.sin(rad)
15
15
  s_cos = 1 - n_cos
@@ -111,6 +111,10 @@ module Geom
111
111
  v.z = vx * ma[2][0] + vy * ma[2][1] + vz * ma[2][2]
112
112
  end
113
113
 
114
+ def *(other)
115
+ multiply(other)
116
+ end
117
+
114
118
  def multiply(other)
115
119
  # matrix multiplication is m[r][c] = (row[r]).(col[c])
116
120
  s = to_a
@@ -133,7 +137,8 @@ module Geom
133
137
  Matrix3D[
134
138
  [rm00, rm01, rm02, rm03],
135
139
  [rm10, rm11, rm12, rm13],
136
- [rm20, rm21, rm22, rm23]
140
+ [rm20, rm21, rm22, rm23],
141
+ [ 0, 0, 0, 1]
137
142
  ]
138
143
  end
139
144
 
data/lib/geom/number.rb CHANGED
@@ -72,6 +72,14 @@ module Geom
72
72
  )
73
73
  end
74
74
 
75
+ def hash
76
+ "#{@x}#{@y}#{@z}".hash
77
+ end
78
+
79
+ def eql?(other)
80
+ self == other
81
+ end
82
+
75
83
  def == (other)
76
84
  @x == other.x &&
77
85
  @y == other.y &&
data/lib/geom/polygon.rb CHANGED
@@ -124,30 +124,26 @@ module Geom
124
124
  end
125
125
 
126
126
  def point_inside(pt)
127
- x = "x"
128
- y = "y"
129
- n = @vertices.length
130
- dominant = dominant_axis
131
- dist = self.plane.distance(pt)
132
- result = false
133
-
134
- return false if dist.abs > 0.01
135
- case dominant
136
- when AXIS_X
137
- x = "y"
138
- y = "z"
139
- when AXIS_Y
140
- y = "z"
141
- end
142
-
143
- @vertices.each_with_index do |v,i|
144
- vn = @vertices[(i+1)%n] # next
145
- if (((v.send(y) <= pt.send(y)) && (pt.send(y) < vn.send(y))) || ((vn.send(y) <= pt.send(y)) && (pt.send(y) < v.send(y)))) &&
146
- (pt.send(x) < (vn.send(x) - v.send(x)) * (pt.send(y) - v.send(y)) / (vn.send(y) - v.send(y)) + v.send(x))
147
- result = !result
127
+ x = pt.x;
128
+ y = pt.y;
129
+ c = false;
130
+ length = @vertices.length
131
+
132
+ length.times do |i|
133
+ j = (i+1) % length
134
+
135
+ a = @vertices[i]
136
+ b = @vertices[j]
137
+
138
+ xpi = a.x
139
+ ypi = a.y
140
+ xpj = b.x
141
+ ypj = b.y
142
+ if ((((ypi<=y) && (y<ypj)) || ((ypj<=y) && (y<ypi))) && (x < (xpj-xpi)*(y-ypi)/(ypj-ypi)+xpi))
143
+ c = !c
148
144
  end
149
145
  end
150
- result
146
+ return c;
151
147
  end
152
148
 
153
149
  def winding
data/lib/geom/vertex.rb CHANGED
@@ -12,6 +12,14 @@ module Geom
12
12
  @normal = normal
13
13
  end
14
14
 
15
+ def hash
16
+ @position.hash
17
+ end
18
+
19
+ def eql?(other)
20
+ self == other
21
+ end
22
+
15
23
  def == (other)
16
24
  @position == other.position
17
25
  end