RUIC 0.4.1 → 0.4.2

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