RUIC 0.4.2 → 0.4.3

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,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