fml 0.2.3 → 0.2.4

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