RUIC 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,165 +1,165 @@
1
- #encoding: utf-8
2
- class UIC::Property
3
- class << self; attr_accessor :default; end
4
- def initialize(el); @el = el; end
5
- def name; @name||=@el['name']; end
6
- def type; @type||=@el['type']; end
7
- def formal; @formal||=@el['formalName'] || @el['name']; end
8
- def min; @el['min']; end
9
- def max; @el['max']; end
10
- def description; @desc||=@el['description']; end
11
- def default; @def ||= (@el['default'] || self.class.default); end
12
- def get(asset,slide)
13
- if asset.slide? || asset.has_slide?(slide)
14
- asset.presentation.get_attribute(asset,name,slide) || default
15
- end
16
- end
17
- def set(asset,new_value,slide_name_or_index)
18
- asset.presentation.set_attribute(asset,name,slide_name_or_index,new_value)
19
- end
20
- def inspect
21
- "<#{type} '#{name}'>"
22
- end
23
-
24
- class String < self
25
- self.default = ''
26
- end
27
- MultiLineString = String
28
-
29
- class Float < self
30
- self.default = 0.0
31
- def get(asset,slide); super.to_f; end
32
- end
33
- class Long < self
34
- self.default = 0
35
- def get(asset,slide); super.to_i; end
36
- end
37
- class Boolean < self
38
- self.default = false
39
- def get(asset,slide); super=='True'; end
40
- def set(asset,new_value,slide_name_or_index)
41
- super( asset, new_value ? 'True' : 'False', slide_name_or_index )
42
- end
43
- end
44
- class Vector < self
45
- self.default = '0 0 0'
46
- def get(asset,slide)
47
- VectorValue.new(asset,self,slide,super)
48
- end
49
- def set(asset,new_value,slide_name_or_index)
50
- new_value = new_value.join(' ') if new_value.is_a?(Array)
51
- super( asset, new_value, slide_name_or_index )
52
- end
53
- end
54
- Rotation = Vector
55
- Color = Vector
56
- Float2 = Vector
57
- class Image < self
58
- self.default = nil
59
- def get(asset,slide)
60
- if idref = super
61
- result = asset.presentation.asset_by_id( idref[1..-1] )
62
- slide ? result.on_slide( slide ) : result
63
- end
64
- end
65
- def set(asset,new_value,slide)
66
- raise "Setting image attributes not yet supported"
67
- end
68
- end
69
- class Texture < String
70
- def get(asset,slide)
71
- if path=super
72
- path.empty? ? nil : path.gsub( '\\', '/' ).sub( /^.\// ,'' )
73
- end
74
- end
75
- end
76
-
77
- class ObjectRef < self
78
- self.default = nil
79
- def get(asset,slide)
80
- ref = super
81
- type = :absolute
82
- obj = nil
83
- unless ref=='' || ref.nil?
84
- type = ref[0]=='#' ? :absolute : :path
85
- ref = type==:absolute ? asset.presentation.asset_by_id( ref[1..-1] ) : asset.presentation.at( ref, asset )
86
- end
87
- ObjectReference.new(asset,self,slide,ref,type)
88
- end
89
- def set(asset,new_object,slide)
90
- get(asset,slide).object = new_object
91
- end
92
- end
93
-
94
- class ObjectReference
95
- attr_reader :object, :type
96
- def initialize(asset,property,slide,object=nil,type=nil)
97
- @asset = asset
98
- @name = property.name
99
- @slide = slide
100
- @object = object
101
- @type = type
102
- end
103
- def object=(new_object)
104
- raise "ObjectRef must be set to an asset (not a #{new_object.class.name})" unless new_object.is_a?(UIC::MetaData::Root)
105
- @object = new_object
106
- write_value!
107
- end
108
- def type=(new_type)
109
- raise "ObjectRef types must be either :absolute or :path (not #{new_type.inspect})" unless [:absolute,:path].include?(new_type)
110
- @type = new_type
111
- write_value!
112
- end
113
- private
114
- def write_value!
115
- path = case @object
116
- when NilClass then ""
117
- else case @type
118
- when :absolute then "##{@object.el['id']}"
119
- when :path then @asset.presentation.path_to( @object, @asset ).sub(/^[^:.]+:/,'')
120
- # when :root then @asset.presentation.path_to( @object ).sub(/^[^:.]+:/,'')
121
- end
122
- end
123
- @asset.presentation.set_attribute( @asset, @name, @slide, path )
124
- end
125
- end
126
-
127
- Import = String #TODO: a real class
128
- Mesh = String #TODO: a real class
129
- Renderable = String #TODO: a real class
130
- Font = String #TODO: a real class
131
- FontSize = Long
132
-
133
- StringListOrInt = String #TODO: a real class
134
-
135
- class VectorValue
136
- attr_reader :x, :y, :z
137
- def initialize(asset,property,slide,str)
138
- @asset = asset
139
- @property = property
140
- @slide = slide
141
- @x, @y, @z = str.split(/\s+/).map(&:to_f)
142
- end
143
- def setall
144
- @property.set( @asset, to_s, @slide )
145
- end
146
- def x=(n); @x=n; setall end
147
- def y=(n); @y=n; setall end
148
- def z=(n); @z=n; setall end
149
- alias_method :r, :x
150
- alias_method :g, :y
151
- alias_method :b, :z
152
- alias_method :r=, :x=
153
- alias_method :g=, :y=
154
- alias_method :b=, :z=
155
- def inspect
156
- "<#{@asset.path}.#{@property.name}: #{self}>"
157
- end
158
- def to_s
159
- to_a.join(' ')
160
- end
161
- def to_a
162
- [x,y,z]
163
- end
164
- end
165
- end
1
+ #encoding: utf-8
2
+ class UIC::Property
3
+ class << self; attr_accessor :default; end
4
+ def initialize(el); @el = el; end
5
+ def name; @name||=@el['name']; end
6
+ def type; @type||=@el['type']; end
7
+ def formal; @formal||=@el['formalName'] || @el['name']; end
8
+ def min; @el['min']; end
9
+ def max; @el['max']; end
10
+ def description; @desc||=@el['description']; end
11
+ def default; @def ||= (@el['default'] || self.class.default); end
12
+ def get(asset,slide)
13
+ if asset.slide? || asset.has_slide?(slide)
14
+ asset.presentation.get_attribute(asset,name,slide) || default
15
+ end
16
+ end
17
+ def set(asset,new_value,slide_name_or_index)
18
+ asset.presentation.set_attribute(asset,name,slide_name_or_index,new_value)
19
+ end
20
+ def inspect
21
+ "<#{type} '#{name}'>"
22
+ end
23
+
24
+ class String < self
25
+ self.default = ''
26
+ end
27
+ MultiLineString = String
28
+
29
+ class Float < self
30
+ self.default = 0.0
31
+ def get(asset,slide); super.to_f; end
32
+ end
33
+ class Long < self
34
+ self.default = 0
35
+ def get(asset,slide); super.to_i; end
36
+ end
37
+ class Boolean < self
38
+ self.default = false
39
+ def get(asset,slide); super=='True'; end
40
+ def set(asset,new_value,slide_name_or_index)
41
+ super( asset, new_value ? 'True' : 'False', slide_name_or_index )
42
+ end
43
+ end
44
+ class Vector < self
45
+ self.default = '0 0 0'
46
+ def get(asset,slide)
47
+ VectorValue.new(asset,self,slide,super)
48
+ end
49
+ def set(asset,new_value,slide_name_or_index)
50
+ new_value = new_value.join(' ') if new_value.is_a?(Array)
51
+ super( asset, new_value, slide_name_or_index )
52
+ end
53
+ end
54
+ Rotation = Vector
55
+ Color = Vector
56
+ Float2 = Vector
57
+ class Image < self
58
+ self.default = nil
59
+ def get(asset,slide)
60
+ if idref = super
61
+ result = asset.presentation.asset_by_id( idref[1..-1] )
62
+ slide ? result.on_slide( slide ) : result
63
+ end
64
+ end
65
+ def set(asset,new_value,slide)
66
+ raise "Setting image attributes not yet supported"
67
+ end
68
+ end
69
+ class Texture < String
70
+ def get(asset,slide)
71
+ if path=super
72
+ path.empty? ? nil : path.gsub( '\\', '/' ).sub( /^.\// ,'' )
73
+ end
74
+ end
75
+ end
76
+
77
+ class ObjectRef < self
78
+ self.default = nil
79
+ def get(asset,slide)
80
+ ref = super
81
+ type = :absolute
82
+ obj = nil
83
+ unless ref=='' || ref.nil?
84
+ type = ref[0]=='#' ? :absolute : :path
85
+ ref = type==:absolute ? asset.presentation.asset_by_id( ref[1..-1] ) : asset.presentation.at( ref, asset )
86
+ end
87
+ ObjectReference.new(asset,self,slide,ref,type)
88
+ end
89
+ def set(asset,new_object,slide)
90
+ get(asset,slide).object = new_object
91
+ end
92
+ end
93
+
94
+ class ObjectReference
95
+ attr_reader :object, :type
96
+ def initialize(asset,property,slide,object=nil,type=nil)
97
+ @asset = asset
98
+ @name = property.name
99
+ @slide = slide
100
+ @object = object
101
+ @type = type
102
+ end
103
+ def object=(new_object)
104
+ raise "ObjectRef must be set to an asset (not a #{new_object.class.name})" unless new_object.is_a?(UIC::MetaData::AssetBase)
105
+ @object = new_object
106
+ write_value!
107
+ end
108
+ def type=(new_type)
109
+ raise "ObjectRef types must be either :absolute or :path (not #{new_type.inspect})" unless [:absolute,:path].include?(new_type)
110
+ @type = new_type
111
+ write_value!
112
+ end
113
+ private
114
+ def write_value!
115
+ path = case @object
116
+ when NilClass then ""
117
+ else case @type
118
+ when :absolute then "##{@object.el['id']}"
119
+ when :path then @asset.presentation.path_to( @object, @asset ).sub(/^[^:.]+:/,'')
120
+ # when :root then @asset.presentation.path_to( @object ).sub(/^[^:.]+:/,'')
121
+ end
122
+ end
123
+ @asset.presentation.set_attribute( @asset, @name, @slide, path )
124
+ end
125
+ end
126
+
127
+ Import = String #TODO: a real class
128
+ Mesh = String #TODO: a real class
129
+ Renderable = String #TODO: a real class
130
+ Font = String #TODO: a real class
131
+ FontSize = Long
132
+
133
+ StringListOrInt = String #TODO: a real class
134
+
135
+ class VectorValue
136
+ attr_reader :x, :y, :z
137
+ def initialize(asset,property,slide,str)
138
+ @asset = asset
139
+ @property = property
140
+ @slide = slide
141
+ @x, @y, @z = str.split(/\s+/).map(&:to_f)
142
+ end
143
+ def setall
144
+ @property.set( @asset, to_s, @slide )
145
+ end
146
+ def x=(n); @x=n; setall end
147
+ def y=(n); @y=n; setall end
148
+ def z=(n); @z=n; setall end
149
+ alias_method :r, :x
150
+ alias_method :g, :y
151
+ alias_method :b, :z
152
+ alias_method :r=, :x=
153
+ alias_method :g=, :y=
154
+ alias_method :b=, :z=
155
+ def inspect
156
+ "<#{@asset.path}.#{@property.name}: #{self}>"
157
+ end
158
+ def to_s
159
+ to_a.join(' ')
160
+ end
161
+ def to_a
162
+ [x,y,z]
163
+ end
164
+ end
165
+ end
@@ -21,6 +21,7 @@ end
21
21
 
22
22
  class UIC::Application::Behavior < UIC::Behavior
23
23
  include UIC::ElementBacked
24
+ # @!parse extend UIC::ElementBacked::ClassMethods
24
25
  xmlattribute :id
25
26
  xmlattribute :src
26
27
  def initialize(application,el)
@@ -1,35 +1,73 @@
1
+ # Supports classes that represent an XML file on disk (e.g. `.uia` and `.uip`).
1
2
  module UIC::FileBacked
2
- attr_accessor :doc, :file
3
+ # @return [Nokogiri::XML::Document] the Nokogiri document representing the instance.
4
+ attr_accessor :doc
5
+
6
+ # @return [String] the absolute path to the underlying file.
7
+ attr_accessor :file
8
+
9
+ # @param relative [String] a file path relative to this file.
10
+ # @return [String] the full path resolved relative to this file.
3
11
  def path_to( relative )
4
12
  File.expand_path( relative.gsub('\\','/'), File.dirname(file) )
5
13
  end
14
+
15
+ # @return [String] the name of the file (without any directories).
6
16
  def filename
7
17
  File.basename(file)
8
18
  end
19
+
20
+ # @return [Boolean] `true` if the underlying file exists on disk.
9
21
  def file_found?
10
- !@file_not_found
22
+ @file && File.exist?(@file)
11
23
  end
24
+
25
+ # Set the file for the class. Does **not** attempt to load the XML document.
26
+ # @param new_path [String] the file path for this class.
27
+ # @return [String]
12
28
  def file=( new_path )
13
29
  @file = File.expand_path(new_path)
14
- @file_not_found = !File.exist?(new_path)
15
30
  end
31
+
32
+ # @return [String] the XML representation of the document.
16
33
  def to_xml
17
34
  doc.to_xml( indent:1, indent_text:"\t" )
18
35
  end
36
+
37
+ # Overwrite the associated file on disk with the {#to_xml} representation of this class.
38
+ # @return [true]
19
39
  def save!
20
40
  File.open(file,'w:utf-8'){ |f| f << to_xml }
41
+ true
42
+ end
43
+
44
+ # Save to the supplied file path. Subsequent calls to {#save!} will save to the new file, not the original file name.
45
+ def save_as(new_file)
46
+ File.open(new_file,'w:utf-8'){ |f| f << to_xml }
47
+ self.file = new_file
21
48
  end
22
49
  end
23
50
 
51
+ # Supports classes that represent an XML element (e.g. `<presentation id="main" src="foo.uip"/>`).
24
52
  module UIC::ElementBacked
25
- attr_accessor :owner, :el
53
+
54
+ # @return [Object] the object in charge of this instance.
55
+ attr_accessor :owner
56
+
57
+ # @return [Nokogiri::XML::Element] the element backing this instance.
58
+ attr_accessor :el
59
+
60
+ # @private
26
61
  def self.included(base)
27
62
  base.extend(ClassMethods)
28
63
  end
64
+
29
65
  module ClassMethods
30
- def xmlattribute(name,&block)
31
- define_method(name){ @el[name] }
32
- define_method("#{name}=", &(block || ->(new_value){ @el[name]=new_value.to_s }))
66
+ # @param name [String] the name of an XML attribute to expose.
67
+ # @param getblock [Proc] a proc to run
68
+ def xmlattribute(name,getblock=nil,&setblock)
69
+ define_method(name){ getblock ? getblock[@el[name]] : @el[name] }
70
+ define_method("#{name}="){ |new_value| @el[name] = (setblock ? setblock[new_value] : new_value).to_s }
33
71
  end
34
72
  end
35
73
  end
@@ -52,16 +52,13 @@ class UIC::Presentation
52
52
  @slides_by_el = {} # indexed by slide state element
53
53
  end
54
54
 
55
+ # @return [String] the xml representation of this presentation. Formatted to match UI Composer Studio's formatting as closely as possible (for minimal diffs after update).
55
56
  def to_xml
56
57
  doc.to_xml( indent:1, indent_text:"\t" )
57
58
  .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
58
59
  .sub('"?>','" ?>')
59
60
  end
60
61
 
61
- def save_as(new_file)
62
- File.open(new_file,'w:utf-8'){ |f| f << to_xml }
63
- end
64
-
65
62
  # Update the presentation to be in-sync with the document.
66
63
  # Must be called whenever the in-memory representation of the XML document is changed.
67
64
  # Called automatically by all necessary methods; only necessary if script (dangerously)
@@ -90,12 +87,12 @@ class UIC::Presentation
90
87
 
91
88
  # Find an asset in the presentation based on its internal XML identifier.
92
89
  # @param id [String] the id of the asset (not an idref), e.g. `"Material_003"`.
93
- # @return [MetaData::Root] the found asset, or `nil` if could not be found.
90
+ # @return [MetaData::AssetBase] the found asset, or `nil` if could not be found.
94
91
  def asset_by_id( id )
95
92
  (@graph_by_id[id] && asset_for_el( @graph_by_id[id] ))
96
93
  end
97
94
 
98
- # @param asset [MetaData::Root] an asset in the presentation
95
+ # @param asset [MetaData::AssetBase] an asset in the presentation
99
96
  # @return [Integer] the index of the first slide where an asset is added (0 for master, non-zero for non-master).
100
97
  def slide_index(asset)
101
98
  # TODO: probably faster to .find the first @addsets_by_graph
@@ -104,8 +101,8 @@ class UIC::Presentation
104
101
  (slide ? slide.xpath('count(ancestor::State) + count(preceding-sibling::State[ancestor::State])').to_i : 0) # the Scene is never added
105
102
  end
106
103
 
107
- # @param child_asset [MetaData::Root] an asset in the presentation.
108
- # @return [MetaData::Root] the scene graph parent of the child asset, or `nil` for the Scene.
104
+ # @param child_asset [MetaData::AssetBase] an asset in the presentation.
105
+ # @return [MetaData::AssetBase] the scene graph parent of the child asset, or `nil` for the Scene.
109
106
  def parent_asset( child_asset )
110
107
  child_graph_el = child_asset.el
111
108
  unless child_graph_el==@scene || child_graph_el.parent.nil?
@@ -113,8 +110,8 @@ class UIC::Presentation
113
110
  end
114
111
  end
115
112
 
116
- # @param parent_asset [MetaData::Root] an asset in the presentation.
117
- # @return [Array<MetaData::Root>] array of scene graph children of the specified asset.
113
+ # @param parent_asset [MetaData::AssetBase] an asset in the presentation.
114
+ # @return [Array<MetaData::AssetBase>] array of scene graph children of the specified asset.
118
115
  def child_assets( parent_asset )
119
116
  parent_asset.el.element_children.map{ |child| asset_for_el(child) }
120
117
  end
@@ -166,14 +163,13 @@ class UIC::Presentation
166
163
  end
167
164
  private :asset_for_el
168
165
 
169
-
170
- def referenced_files
171
- (
172
- (images + behaviors + effects + meshes + materials ).map(&:file)
173
- + effects.flat_map(&:images)
174
- + fonts
175
- ).sort_by{ |f| parts = f.split(/[\/\\]/); [parts.length,parts] }
176
- end
166
+ # def referenced_files
167
+ # (
168
+ # (images + behaviors + effects + meshes + materials ).map(&:file)
169
+ # + effects.flat_map(&:images)
170
+ # + fonts
171
+ # ).sort_by{ |f| parts = f.split(/[\/\\]/); [parts.length,parts] }
172
+ # end
177
173
 
178
174
  # @return [MetaData::Scene] the root scene asset for the presentation.
179
175
  def scene
@@ -185,8 +181,8 @@ class UIC::Presentation
185
181
  # * If `from_asset` is supplied the path will be relative to that asset (e.g. `"parent.parent.Group.Model"`).
186
182
  # * If `from_asset` is omitted the path will be absolute (e.g. `"Scene.Layer.Group.Model"`).
187
183
  #
188
- # @param asset [MetaData::Root] the asset to find the path to.
189
- # @param from_asset [MetaData::Root] the asset to find the path relative to.
184
+ # @param asset [MetaData::AssetBase] the asset to find the path to.
185
+ # @param from_asset [MetaData::AssetBase] the asset to find the path relative to.
190
186
  # @return [String] the script path to the element.
191
187
  def path_to( asset, from_asset=nil )
192
188
  el = asset.el
@@ -244,7 +240,10 @@ class UIC::Presentation
244
240
  #
245
241
  # assert layer1==layer2 && layer2==layer3 && layer3==layer4
246
242
  #
247
- # @return [MetaData::Root] The found asset, or `nil` if it cannot be found.
243
+ # @return [MetaData::AssetBase] The found asset, or `nil` if it cannot be found.
244
+ #
245
+ # @see Application#at
246
+ # @see MetaData::AssetBase#at
248
247
  def at(path,root=@graph)
249
248
  name,path = path.split('.',2)
250
249
  root = root.el if root.respond_to?(:el)
@@ -267,7 +266,7 @@ class UIC::Presentation
267
266
  #
268
267
  # assert preso.get_attribute(camera,'position',0) == camera['position',0]
269
268
  #
270
- # @param asset [MetaData::Root] the asset to fetch the attribute for.
269
+ # @param asset [MetaData::AssetBase] the asset to fetch the attribute for.
271
270
  # @param attr_name [String] the name of the attribute to get the value of.
272
271
  # @param slide_name_or_index [String,Integer] the string name or integer index of the slide.
273
272
  def get_attribute( asset, attr_name, slide_name_or_index )
@@ -293,7 +292,7 @@ class UIC::Presentation
293
292
  # # …and the shorter way
294
293
  # camera['endtime',0] = 1000
295
294
  #
296
- # @param asset [MetaData::Root] the asset to fetch the attribute for.
295
+ # @param asset [MetaData::AssetBase] the asset to fetch the attribute for.
297
296
  # @param attr_name [String] the name of the attribute to get the value of.
298
297
  # @param slide_name_or_index [String,Integer] the string name or integer index of the slide.
299
298
  def set_attribute( asset, property_name, slide_name_or_index, str )
@@ -319,14 +318,14 @@ class UIC::Presentation
319
318
  end
320
319
  end
321
320
 
322
- # @return [MetaData::Root] the component (or Scene) asset that owns the supplied asset.
323
- # @see MetaData::Root#component
321
+ # @return [MetaData::AssetBase] the component (or Scene) asset that owns the supplied asset.
322
+ # @see MetaData::AssetBase#component
324
323
  def owning_component( asset )
325
324
  asset_for_el( owning_component_element( asset.el ) )
326
325
  end
327
326
 
328
- # @return [MetaData::Root] the component asset that owns the supplied asset.
329
- # @see MetaData::Root#component
327
+ # @return [MetaData::AssetBase] the component asset that owns the supplied asset.
328
+ # @see MetaData::AssetBase#component
330
329
  def owning_component_element( graph_element )
331
330
  graph_element.at_xpath('(ancestor::Component[1] | ancestor::Scene[1])[last()]')
332
331
  end
@@ -345,9 +344,9 @@ class UIC::Presentation
345
344
  end
346
345
  private :master_slide_for
347
346
 
348
- # @param asset [MetaData::Root] the asset to get the slides for.
347
+ # @param asset [MetaData::AssetBase] the asset to get the slides for.
349
348
  # @return [SlideCollection] an array-like collection of all slides that the asset is available on.
350
- # @see MetaData::Root#slides
349
+ # @see MetaData::AssetBase#slides
351
350
  def slides_for( asset )
352
351
  graph_element = asset.el
353
352
  @slides_for[graph_element] ||= begin
@@ -361,7 +360,7 @@ class UIC::Presentation
361
360
  end
362
361
 
363
362
  # @return [Boolean] true if the asset exists on the supplied slide.
364
- # @see MetaData::Root#has_slide?
363
+ # @see MetaData::AssetBase#has_slide?
365
364
  def has_slide?( asset, slide_name_or_index )
366
365
  graph_element = asset.el
367
366
  if graph_element == @scene
@@ -389,7 +388,7 @@ class UIC::Presentation
389
388
 
390
389
  # Unlinks a master attribute, yielding distinct values on each slide. If the asset is not on the master slide, or the attribute is already unlinked, no change occurs.
391
390
  #
392
- # @param asset [MetaData::Root] the master asset to unlink the attribute on.
391
+ # @param asset [MetaData::AssetBase] the master asset to unlink the attribute on.
393
392
  # @param attribute_name [String] the name of the attribute to unlink.
394
393
  # @return [Boolean] `true` if the attribute was previously linked; `false` otherwise.
395
394
  def unlink_attribute(asset,attribute_name)
@@ -409,10 +408,10 @@ class UIC::Presentation
409
408
 
410
409
  # Replace an existing asset with a new kind of asset.
411
410
  #
412
- # @param existing_asset [MetaData::Root] the existing asset to replace.
411
+ # @param existing_asset [MetaData::AssetBase] the existing asset to replace.
413
412
  # @param new_type [String] the name of the asset type, e.g. `"ReferencedMaterial"` or `"Group"`.
414
413
  # @param attributes [Hash] initial attribute values for the new asset.
415
- # @return [MetaData::Root] the newly-created asset.
414
+ # @return [MetaData::AssetBase] the newly-created asset.
416
415
  def replace_asset( existing_asset, new_type, attributes={} )
417
416
  old_el = existing_asset.el
418
417
  new_el = old_el.replace( "<#{new_type}/>" ).first
@@ -431,13 +430,65 @@ class UIC::Presentation
431
430
  (graph_element == @scene) || !!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][0])
432
431
  end
433
432
 
434
- def find(options={})
433
+ # Find assets in this presentation matching criteria.
434
+ #
435
+ # @example 1) Searching for simple values
436
+ # every_asset = preso.find # Every asset in the presentation
437
+ # master_assets = preso.find _master:true # Test for master/nonmaster
438
+ # models = preso.find _type:'Model' # …or based on type
439
+ # slide2_assets = preso.find _slide:2 # …or presence on a specific slide
440
+ # rectangles = preso.find sourcepath:'#Rectangle' # …or attribute values
441
+ # gamecovers = preso.find name:'Game Cover' # …including the name
442
+ #
443
+ # @example 2) Combine tests to get more specific
444
+ # master_models = preso.find _type:'Model', _master:true
445
+ # slide2_rects = preso.find _type:'Model', _slide:2, sourcepath:'#Rectangle'
446
+ # nonmaster_s2 = preso.find _slide:2, _master:false
447
+ # red_materials = preso.find _type:'Material', diffuse:[1,0,0]
448
+ #
449
+ # @example 3) Matching values more loosely
450
+ # pistons = preso.find name:/^Piston/ # Regex for batch finding
451
+ # bottom_row = preso.find position:[nil,-200,nil] # nil for wildcards in vectors
452
+ #
453
+ # @example 4) Restrict the search to a sub-tree
454
+ # group = preso/"Scene.Layer.Group"
455
+ # group_models = preso.find _under:group, _type:'Model' # All models under the group
456
+ # group_models = group.find _type:'Model' # alternatively start from the asset
457
+ #
458
+ # @example 5) Iterate the results as they are found
459
+ # preso.find _type:'Model', name:/^Piston/ do |model, index|
460
+ # show "Model #{index} is named #{model.name}"
461
+ # end
462
+ #
463
+ # # You do not need to receive the index if you do not need its value
464
+ # group.find _type:'Model' do |model|
465
+ # scale = model['scale'].value
466
+ # scale.x = scale.y = scale.z = 1
467
+ # end
468
+ #
469
+ # @param criteria [Hash] Attribute names and values, along with a few special keys.
470
+ # @option criteria :_type [String] asset must be of specified type, e.g. `"Model"` or `"Material"` or `"PathAnchorPoint"`.
471
+ # @option criteria :_slide [Integer,String] slide number or name that the asset must be present on.
472
+ # @option criteria :_master [Boolean] `true` for only master assets, `false` for only non-master assets.
473
+ # @option criteria :_under [MetaData::AssetBase] a root asset to require as an ancestor; this asset will never be in the search results.
474
+ # @option criteria :attribute_name [Numeric] numeric attribute value must be within `0.001` of the supplied value.
475
+ # @option criteria :attribute_name [String] string attribute value must match the supplied string exactly.
476
+ # @option criteria :attribute_name [Regexp] supplied regex must match the string attribute value.
477
+ # @option criteria :attribute_name [Array] each component of the attribute's vector value must be within `0.001` of the supplied value in the array;
478
+ # if a value in the array is `nil` that component of the vector may have any value.
479
+ #
480
+ # @yield [asset,index] Yields each found asset (and its index in the results) to the block (if supplied) as they are found.
481
+ #
482
+ # @return [Array<MetaData::AssetBase>] array of all matching assets (may be empty).
483
+ #
484
+ # @see MetaData::AssetBase#find
485
+ def find(criteria={},&block)
435
486
  index = -1
436
- start = options.key?(:_under) ? options.delete(:_under).el : @graph
487
+ start = criteria.key?(:_under) ? criteria.delete(:_under).el : @graph
437
488
  [].tap do |result|
438
489
  start.xpath('./descendant::*').each do |el|
439
490
  asset = asset_for_el(el)
440
- next unless options.all? do |att,val|
491
+ next unless criteria.all? do |att,val|
441
492
  case att
442
493
  when :_type then el.name == val
443
494
  when :_slide then has_slide?(asset,val)
@@ -460,6 +511,7 @@ class UIC::Presentation
460
511
  end
461
512
  end
462
513
 
514
+ # @private
463
515
  def inspect
464
516
  "<#{self.class} #{File.basename(file)}>"
465
517
  end
@@ -471,14 +523,25 @@ end
471
523
 
472
524
  class UIC::Application::Presentation < UIC::Presentation
473
525
  include UIC::ElementBacked
474
- xmlattribute :id
475
- xmlattribute :src
526
+ # @!parse extend UIC::ElementBacked::ClassMethods
527
+
528
+ # @!attribute [rw] id
529
+ # @return [String] the id of the presentation asset
476
530
  xmlattribute :id do |new_id|
477
531
  main_preso = app.main_presentation
478
532
  super(new_id)
479
533
  app.main_presentation=self if main_preso==self
480
534
  end
481
- xmlattribute :active
535
+
536
+ # @!attribute src
537
+ # @return [String] the path to the presentation file
538
+ xmlattribute :src
539
+
540
+ # @!attribute active
541
+ # @return [Boolean] is the presentation initially active?
542
+ xmlattribute :active, ->(val){ val=="True" } do |new_val|
543
+ new_val ? 'True' : 'False'
544
+ end
482
545
 
483
546
  def initialize(application,el)
484
547
  self.owner = application
@@ -492,6 +555,7 @@ class UIC::Application::Presentation < UIC::Presentation
492
555
  end
493
556
  end
494
557
 
558
+ # @private no need to document this monkeypatch
495
559
  class Nokogiri::XML::Element
496
560
  def index(kind='*') # Find the index of this element amongs its siblings
497
561
  xpath("count(./preceding-sibling::#{kind})").to_i