RUIC 0.4.1 → 0.4.2

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.el,name,slide) || default
15
- end
16
- end
17
- def set(asset,new_value,slide_name_or_index)
18
- asset.presentation.set_attribute(asset.el,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.el )
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::Asset::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.el, @asset.el ).sub(/^[^:.]+:/,'')
120
- # when :root then @asset.presentation.path_to( @object.el ).sub(/^[^:.]+:/,'')
121
- end
122
- end
123
- @asset.presentation.set_attribute( @asset.el, @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::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
File without changes
@@ -12,7 +12,12 @@ module UIC::FileBacked
12
12
  def file=( new_path )
13
13
  @file = File.expand_path(new_path)
14
14
  @file_not_found = !File.exist?(new_path)
15
- # warn "Could not find file '#{new_path}'" unless file_found?
15
+ end
16
+ def to_xml
17
+ doc.to_xml( indent:1, indent_text:"\t" )
18
+ end
19
+ def save!
20
+ File.open(file,'w:utf-8'){ |f| f << to_xml }
16
21
  end
17
22
  end
18
23
 
@@ -26,7 +31,7 @@ module UIC::ElementBacked
26
31
  define_method(name){ @el[name] }
27
32
  define_method("#{name}=", &(block || ->(new_value){ @el[name]=new_value.to_s }))
28
33
  end
29
- end
34
+ end
30
35
  end
31
36
 
32
37
  module UIC::PresentableHash
@@ -1,11 +1,21 @@
1
+ # A `Presentation` represents a `.uip` presentation, created and edited by UI Composer Studio.
1
2
  class UIC::Presentation
2
3
  include UIC::FileBacked
3
- def initialize( uip_path )
4
+
5
+ # Create a new presentation. If you do not specify the `uip_path` to load from, you must
6
+ # later set the `.file = `for the presentation, and then call the {#load_from_file} method.
7
+ # @param uip_path [String] path to the `.uip` to load.
8
+ def initialize( uip_path=nil )
4
9
  self.file = uip_path
5
10
  load_from_file if file_found?
6
11
  end
7
12
 
13
+ # Load information for the presentation from disk.
14
+ # If you supply a path to a `.uip` file when creating the presentation
15
+ # this method is automatically called.
16
+ # @return [nil]
8
17
  def load_from_file
18
+ # TODO: this method assumes an application to find the metadata on; the metadata should be part of this class instance instead, shared with the app when present
9
19
  @doc = Nokogiri.XML( File.read( file, encoding:'utf-8' ), &:noblanks )
10
20
  @graph = @doc.at('Graph')
11
21
  @scene = @graph.at('Scene')
@@ -32,6 +42,7 @@ class UIC::Presentation
32
42
  app.metadata.create_class( meta, from, reference.name )
33
43
  end
34
44
  @class_by_ref[ "##{reference['id']}" ] = metaklass
45
+ nil
35
46
  end
36
47
 
37
48
  rebuild_caches_from_document
@@ -42,19 +53,21 @@ class UIC::Presentation
42
53
  end
43
54
 
44
55
  def to_xml
45
- @doc.to_xml( indent:1, indent_text:"\t" )
46
- .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
47
- .sub('"?>','" ?>')
48
- end
49
-
50
- def save!
51
- File.open(file,'w:utf-8'){ |f| f << to_xml }
56
+ doc.to_xml( indent:1, indent_text:"\t" )
57
+ .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
58
+ .sub('"?>','" ?>')
52
59
  end
53
60
 
54
61
  def save_as(new_file)
55
62
  File.open(new_file,'w:utf-8'){ |f| f << to_xml }
56
63
  end
57
64
 
65
+ # Update the presentation to be in-sync with the document.
66
+ # Must be called whenever the in-memory representation of the XML document is changed.
67
+ # Called automatically by all necessary methods; only necessary if script (dangerously)
68
+ # manipulates the `.doc` of the presentation directly.
69
+ #
70
+ # @return [nil]
58
71
  def rebuild_caches_from_document
59
72
  @graph_by_id = {}
60
73
  @scene.traverse{ |x| @graph_by_id[x['id']]=x if x.is_a?(Nokogiri::XML::Element) }
@@ -72,27 +85,38 @@ class UIC::Presentation
72
85
  @addsets_by_graph[graph][name] = addset
73
86
  @addsets_by_graph[graph][index] = addset
74
87
  end
88
+ nil
75
89
  end
76
90
 
91
+ # Find an asset in the presentation based on its internal XML identifier.
92
+ # @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.
77
94
  def asset_by_id( id )
78
95
  (@graph_by_id[id] && asset_for_el( @graph_by_id[id] ))
79
96
  end
80
97
 
81
- # Find the index of the slide where an element is added
82
- def slide_index(graph_element)
98
+ # @param asset [MetaData::Root] an asset in the presentation
99
+ # @return [Integer] the index of the first slide where an asset is added (0 for master, non-zero for non-master).
100
+ def slide_index(asset)
83
101
  # TODO: probably faster to .find the first @addsets_by_graph
84
- slide = @logic.at(".//Add[@ref='##{graph_element['id']}']/..")
102
+ id = asset.el['id']
103
+ slide = @logic.at(".//Add[@ref='##{id}']/..")
85
104
  (slide ? slide.xpath('count(ancestor::State) + count(preceding-sibling::State[ancestor::State])').to_i : 0) # the Scene is never added
86
105
  end
87
106
 
88
- def parent_asset( child_graph_el )
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.
109
+ def parent_asset( child_asset )
110
+ child_graph_el = child_asset.el
89
111
  unless child_graph_el==@scene || child_graph_el.parent.nil?
90
112
  asset_for_el( child_graph_el.parent )
91
113
  end
92
114
  end
93
115
 
94
- def child_assets( parent_graph_el )
95
- parent_graph_el.element_children.map{ |child| asset_for_el(child) }
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.
118
+ def child_assets( parent_asset )
119
+ parent_asset.el.element_children.map{ |child| asset_for_el(child) }
96
120
  end
97
121
 
98
122
  # Get an array of all assets in the scene graph, in document order
@@ -100,8 +124,9 @@ class UIC::Presentation
100
124
  @graph_by_id.map{ |id,graph_element| asset_for_el(graph_element) }
101
125
  end
102
126
 
103
- # Returns a hash mapping image paths to arrays of the assets referencing them
127
+ # @return [Hash] a mapping of image paths to arrays of the assets referencing them.
104
128
  def image_usage
129
+ # TODO: this returns the same asset multiple times, with no indication of which property is using it; should switch to an Asset/Property pair, or some such.
105
130
  asset_types = app.metadata.by_name.values + @class_by_ref.values
106
131
 
107
132
  image_properties_by_type = asset_types.flat_map do |type|
@@ -129,16 +154,18 @@ class UIC::Presentation
129
154
  end ].tap{ |h| h.extend(UIC::PresentableHash) }
130
155
  end
131
156
 
157
+ # @return [Array<String>] array of all image paths referenced by this presentation.
132
158
  def image_paths
133
159
  image_usage.keys
134
160
  end
135
161
 
162
+ # Find or create an asset for a scene graph element.
163
+ # @param el [Nokogiri::XML::Element] the scene graph element.
136
164
  def asset_for_el(el)
137
165
  (@asset_by_el[el] ||= el['class'] ? @class_by_ref[el['class']].new(self,el) : app.metadata.new_instance(self,el))
138
166
  end
167
+ private :asset_for_el
139
168
 
140
- attr_reader :addsets_by_graph
141
- protected :addsets_by_graph
142
169
 
143
170
  def referenced_files
144
171
  (
@@ -148,11 +175,22 @@ class UIC::Presentation
148
175
  ).sort_by{ |f| parts = f.split(/[\/\\]/); [parts.length,parts] }
149
176
  end
150
177
 
178
+ # @return [MetaData::Scene] the root scene asset for the presentation.
151
179
  def scene
152
180
  asset_for_el( @scene )
153
181
  end
154
182
 
155
- def path_to( el, from_el=nil )
183
+ # Generate the script path for an asset in the presentation.
184
+ #
185
+ # * If `from_asset` is supplied the path will be relative to that asset (e.g. `"parent.parent.Group.Model"`).
186
+ # * If `from_asset` is omitted the path will be absolute (e.g. `"Scene.Layer.Group.Model"`).
187
+ #
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.
190
+ # @return [String] the script path to the element.
191
+ def path_to( asset, from_asset=nil )
192
+ el = asset.el
193
+
156
194
  to_parts = if el.ancestors('Graph')
157
195
  [].tap{ |parts|
158
196
  until el==@graph
@@ -161,7 +199,8 @@ class UIC::Presentation
161
199
  end
162
200
  }
163
201
  end
164
- if from_el && from_el.ancestors('Graph')
202
+ if from_asset && from_asset.el.ancestors('Graph')
203
+ from_el = from_asset.el
165
204
  from_parts = [].tap{ |parts|
166
205
  until from_el==@graph
167
206
  parts.unshift asset_for_el(from_el).name
@@ -177,16 +216,38 @@ class UIC::Presentation
177
216
  to_parts.join('.')
178
217
  end
179
218
 
219
+ # @return [Boolean] true if there any errors with the presentation.
180
220
  def errors?
181
221
  (!errors.empty?)
182
222
  end
183
223
 
224
+ # @return [Array<String>] an array (possibly empty) of all errors in this presentation.
184
225
  def errors
185
226
  (file_found? ? [] : ["File not found: '#{file}'"])
186
227
  end
187
228
 
229
+ # Find an element or asset in this presentation by scripting path.
230
+ #
231
+ # * If `root` is supplied, the path is resolved relative to that asset.
232
+ # * If `root` is not supplied, the path is resolved as a root-level path.
233
+ #
234
+ # @example
235
+ # preso = app.main
236
+ # scene = preso.scene
237
+ # camera = scene/"Layer.Camera"
238
+ #
239
+ # # Four ways to find the same layer
240
+ # layer1 = preso/"Scene.Layer"
241
+ # layer2 = preso.at "Scene.Layer"
242
+ # layer3 = preso.at "Layer", scene
243
+ # layer4 = preso.at "parent", camera
244
+ #
245
+ # assert layer1==layer2 && layer2==layer3 && layer3==layer4
246
+ #
247
+ # @return [MetaData::Root] The found asset, or `nil` if it cannot be found.
188
248
  def at(path,root=@graph)
189
249
  name,path = path.split('.',2)
250
+ root = root.el if root.respond_to?(:el)
190
251
  el = case name
191
252
  when 'parent' then root==@scene ? nil : root.parent
192
253
  when 'Scene' then @scene
@@ -196,16 +257,48 @@ class UIC::Presentation
196
257
  end
197
258
  alias_method :/, :at
198
259
 
199
- def get_attribute( graph_element, property_name, slide_name_or_index )
260
+ # Fetch the value of an asset's attribute on a particular slide. Slide `0` is the Master Slide, slide `1` is the first non-master slide.
261
+ #
262
+ # This method is used internally by assets; accessing attributes directly from the asset is generally more appropriate.
263
+ #
264
+ # @example
265
+ # preso = app.main_presentation
266
+ # camera = preso/"Scene.Layer.Camera"
267
+ #
268
+ # assert preso.get_attribute(camera,'position',0) == camera['position',0]
269
+ #
270
+ # @param asset [MetaData::Root] the asset to fetch the attribute for.
271
+ # @param attr_name [String] the name of the attribute to get the value of.
272
+ # @param slide_name_or_index [String,Integer] the string name or integer index of the slide.
273
+ def get_attribute( asset, attr_name, slide_name_or_index )
274
+ graph_element = asset.el
200
275
  ((addsets=@addsets_by_graph[graph_element]) && ( # State (slide) don't have any addsets
201
- ( addsets[slide_name_or_index] && addsets[slide_name_or_index][property_name] ) || # Try for a Set on the specific slide
202
- ( addsets[0] && addsets[0][property_name] ) # …else try the master slide
203
- ) || graph_element[property_name]) # …else try the graph
276
+ ( addsets[slide_name_or_index] && addsets[slide_name_or_index][attr_name] ) || # Try for a Set on the specific slide
277
+ ( addsets[0] && addsets[0][attr_name] ) # …else try the master slide
278
+ ) || graph_element[attr_name]) # …else try the graph
204
279
  # TODO: handle animation (child of addset)
205
280
  end
206
281
 
207
- def set_attribute( graph_element, property_name, slide_name_or_index, str )
208
- if attribute_linked?( graph_element, property_name )
282
+ # Set the value of an asset's attribute on a particular slide. Slide `0` is the Master Slide, slide `1` is the first non-master slide.
283
+ #
284
+ # This method is used internally by assets; accessing attributes directly from the asset is generally more appropriate.
285
+ #
286
+ # @example
287
+ # preso = app.main_presentation
288
+ # camera = preso/"Scene.Layer.Camera"
289
+ #
290
+ # # The long way to set the attribute value
291
+ # preso.set_attribute(camera,'endtime',0,1000)
292
+ #
293
+ # # …and the shorter way
294
+ # camera['endtime',0] = 1000
295
+ #
296
+ # @param asset [MetaData::Root] the asset to fetch the attribute for.
297
+ # @param attr_name [String] the name of the attribute to get the value of.
298
+ # @param slide_name_or_index [String,Integer] the string name or integer index of the slide.
299
+ def set_attribute( asset, property_name, slide_name_or_index, str )
300
+ graph_element = asset.el
301
+ if attribute_linked?( asset, property_name )
209
302
  if @addsets_by_graph[graph_element]
210
303
  @addsets_by_graph[graph_element][0][property_name] = str
211
304
  else
@@ -218,7 +311,7 @@ class UIC::Presentation
218
311
  else
219
312
  master = master_slide_for( graph_element )
220
313
  slide_count = master.xpath('count(./State)').to_i
221
- 0.upto(slide_count).each{ |idx| set_attribute(graph_element,property_name,idx,str) }
314
+ 0.upto(slide_count).each{ |idx| set_attribute(asset,property_name,idx,str) }
222
315
  end
223
316
  else
224
317
  raise "TODO"
@@ -226,24 +319,37 @@ class UIC::Presentation
226
319
  end
227
320
  end
228
321
 
229
- def owning_component( graph_element )
230
- asset_for_el( owning_component_element( graph_element ) )
322
+ # @return [MetaData::Root] the component (or Scene) asset that owns the supplied asset.
323
+ # @see MetaData::Root#component
324
+ def owning_component( asset )
325
+ asset_for_el( owning_component_element( asset.el ) )
231
326
  end
232
327
 
328
+ # @return [MetaData::Root] the component asset that owns the supplied asset.
329
+ # @see MetaData::Root#component
233
330
  def owning_component_element( graph_element )
234
331
  graph_element.at_xpath('(ancestor::Component[1] | ancestor::Scene[1])[last()]')
235
332
  end
333
+ private :owning_component_element
236
334
 
335
+ # @return [Nokogiri::XML::Element] the "time context" scene graph element that owns the supplied element.
237
336
  def owning_or_self_component_element( graph_element )
238
337
  graph_element.at_xpath('(ancestor-or-self::Component[1] | ancestor-or-self::Scene[1])[last()]')
239
338
  end
339
+ private :owning_or_self_component_element
240
340
 
341
+ # @return [Nokogiri::XML::Element] the logic-graph element representing the master slide for a scene graph element
241
342
  def master_slide_for( graph_element )
242
343
  comp = owning_or_self_component_element( graph_element )
243
344
  @logic.at("./State[@component='##{comp['id']}']")
244
345
  end
346
+ private :master_slide_for
245
347
 
246
- def slides_for( graph_element )
348
+ # @param asset [MetaData::Root] the asset to get the slides for.
349
+ # @return [SlideCollection] an array-like collection of all slides that the asset is available on.
350
+ # @see MetaData::Root#slides
351
+ def slides_for( asset )
352
+ graph_element = asset.el
247
353
  @slides_for[graph_element] ||= begin
248
354
  slides = []
249
355
  master = master_slide_for( graph_element )
@@ -254,31 +360,59 @@ class UIC::Presentation
254
360
  end
255
361
  end
256
362
 
257
- def has_slide?( graph_element, slide_name_or_index )
363
+ # @return [Boolean] true if the asset exists on the supplied slide.
364
+ # @see MetaData::Root#has_slide?
365
+ def has_slide?( asset, slide_name_or_index )
366
+ graph_element = asset.el
258
367
  if graph_element == @scene
259
368
  # The scene is never actually added, so we'll treat it just like the first add, which is on the master slide of the scene
260
- has_slide?( @addsets_by_graph.first.first, slide_name_or_index )
369
+ has_slide?( asset_for_el( @addsets_by_graph.first.first ), slide_name_or_index )
261
370
  else
262
371
  @addsets_by_graph[graph_element][slide_name_or_index] || @addsets_by_graph[graph_element][0]
263
372
  end
264
373
  end
265
374
 
266
- def attribute_linked?(graph_element,attribute_name)
375
+ # @example
376
+ # preso = app.main
377
+ # camera = preso/"Scene.Layer.Camera"
378
+ #
379
+ # # Two ways of determining if an attribute for an asset is linked.
380
+ # if preso.attribute_linked?( camera, 'fov' )
381
+ # if camera['fov'].linked?
382
+ #
383
+ # @return [Boolean] true if this asset's attribute is linked on the master slide.
384
+ # @see ValuesPerSlide#linked?
385
+ def attribute_linked?( asset, attribute_name )
386
+ graph_element = asset.el
267
387
  !(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][1] && @addsets_by_graph[graph_element][1].key?(attribute_name))
268
388
  end
269
389
 
270
- def unlink_attribute(graph_element,attribute_name)
271
- if master?(graph_element) && attribute_linked?(graph_element,attribute_name)
272
- master_value = get_attribute( graph_element, attribute_name, 0 )
273
- slides_for( graph_element ).to_ary[1..-1].each do |slide|
390
+ # 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
+ #
392
+ # @param asset [MetaData::Root] the master asset to unlink the attribute on.
393
+ # @param attribute_name [String] the name of the attribute to unlink.
394
+ # @return [Boolean] `true` if the attribute was previously linked; `false` otherwise.
395
+ def unlink_attribute(asset,attribute_name)
396
+ graph_element = asset.el
397
+ if master?(asset) && attribute_linked?(asset,attribute_name)
398
+ master_value = get_attribute( asset, attribute_name, 0 )
399
+ slides_for( asset ).to_ary[1..-1].each do |slide|
274
400
  addset = slide.el.at_xpath( ".//*[@ref='##{graph_element['id']}']" ) || slide.el.add_child("<Set ref='##{graph_element['id']}'/>").first
275
401
  addset[attribute_name] = master_value
276
402
  end
277
403
  rebuild_caches_from_document
278
404
  true
405
+ else
406
+ false
279
407
  end
280
408
  end
281
409
 
410
+ # Replace an existing asset with a new kind of asset.
411
+ #
412
+ # @param existing_asset [MetaData::Root] the existing asset to replace.
413
+ # @param new_type [String] the name of the asset type, e.g. `"ReferencedMaterial"` or `"Group"`.
414
+ # @param attributes [Hash] initial attribute values for the new asset.
415
+ # @return [MetaData::Root] the newly-created asset.
282
416
  def replace_asset( existing_asset, new_type, attributes={} )
283
417
  old_el = existing_asset.el
284
418
  new_el = old_el.replace( "<#{new_type}/>" ).first
@@ -291,8 +425,9 @@ class UIC::Presentation
291
425
  end
292
426
  end
293
427
 
294
- # Is this element added on the master slide?
295
- def master?(graph_element)
428
+ # @return [Boolean] `true` if the asset is added on the master slide.
429
+ def master?(asset)
430
+ graph_element = asset.el
296
431
  (graph_element == @scene) || !!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][0])
297
432
  end
298
433
 
@@ -305,8 +440,8 @@ class UIC::Presentation
305
440
  next unless options.all? do |att,val|
306
441
  case att
307
442
  when :_type then el.name == val
308
- when :_slide then has_slide?(el,val)
309
- when :_master then master?(el)==val
443
+ when :_slide then has_slide?(asset,val)
444
+ when :_master then master?(asset)==val
310
445
  else
311
446
  if asset.properties[att.to_s]
312
447
  value = asset[att.to_s].value