Class: UIC::Presentation

Inherits:
Object
  • Object
show all
Includes:
FileBacked
Defined in:
lib/ruic/presentation.rb

Overview

A Presentation represents a .uip presentation, created and edited by UI Composer Studio.

Direct Known Subclasses

Application::Presentation

Instance Attribute Summary

Attributes included from FileBacked

#doc, #file

Instance Method Summary collapse

Methods included from FileBacked

#file_found?, #filename, #save!, #save_as

Constructor Details

#initialize(uip_path = nil) ⇒ Presentation

Create a new presentation. If you do not specify the uip_path to load from, you must later set the .file =for the presentation, and then call the #load_from_file method.

Parameters:

  • uip_path (String) (defaults to: nil)

    path to the .uip to load.



8
9
10
11
# File 'lib/ruic/presentation.rb', line 8

def initialize( uip_path=nil )
	self.file = uip_path
	load_from_file if file_found?
end

Instance Method Details

#asset_by_id(id) ⇒ MetaData::AssetBase

Find an asset in the presentation based on its internal XML identifier.

Parameters:

  • id (String)

    the id of the asset (not an idref), e.g. "Material_003".

Returns:



91
92
93
# File 'lib/ruic/presentation.rb', line 91

def asset_by_id( id )
	(@graph_by_id[id] && asset_for_el( @graph_by_id[id] ))
end

#assetsObject

Get an array of all assets in the scene graph, in document order



122
123
124
# File 'lib/ruic/presentation.rb', line 122

def assets
	@graph_by_id.map{ |id,graph_element| asset_for_el(graph_element) }
end

#at(path, root = @graph) ⇒ MetaData::AssetBase Also known as: /

Find an element or asset in this presentation by scripting path.

  • If root is supplied, the path is resolved relative to that asset.
  • If root is not supplied, the path is resolved as a root-level path.

Examples:

preso  = app.main
scene  = preso.scene
camera = scene/"Layer.Camera"

# Four ways to find the same layer
layer1 = preso/"Scene.Layer"
layer2 = preso.at "Scene.Layer"
layer3 = preso.at "Layer", scene
layer4 = preso.at "parent", camera

assert layer1==layer2 && layer2==layer3 && layer3==layer4

Returns:

See Also:



263
264
265
266
267
268
269
270
271
272
# File 'lib/ruic/presentation.rb', line 263

def at(path,root=@graph)
	name,path = path.split('.',2)
	root = root.el if root.respond_to?(:el)
	el = case name
		when 'parent' then root==@scene ? nil : root.parent
		when 'Scene'  then @scene
		else               root.element_children.find{ |el| asset_for_el(el).name==name }
	end
	path ? at(path,el) : asset_for_el(el) if el
end

#attribute_linked?(asset, attribute_name) ⇒ Boolean

Returns true if this asset's attribute is linked on the master slide.

Examples:

preso  = app.main
camera = preso/"Scene.Layer.Camera"

# Two ways of determining if an attribute for an asset is linked.
if preso.attribute_linked?( camera, 'fov' )
if camera['fov'].linked?

Returns:

  • (Boolean)

    true if this asset's attribute is linked on the master slide.

See Also:



408
409
410
411
# File 'lib/ruic/presentation.rb', line 408

def attribute_linked?( asset, attribute_name )
	graph_element = asset.el
	!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][1] && @addsets_by_graph[graph_element][1].key?(attribute_name))
end

#child_assets(parent_asset) ⇒ Array<MetaData::AssetBase>

Returns array of scene graph children of the specified asset.

Parameters:

Returns:

See Also:



117
118
119
# File 'lib/ruic/presentation.rb', line 117

def child_assets( parent_asset )
	parent_asset.el.element_children.map{ |child| asset_for_el(child) }
end

#errorsArray<String>

Returns an array (possibly empty) of all errors in this presentation.

Returns:

  • (Array<String>)

    an array (possibly empty) of all errors in this presentation.



237
238
239
# File 'lib/ruic/presentation.rb', line 237

def errors
	(file_found? ? [] : ["File not found: '#{file}'"])
end

#errors?Boolean

Returns true if there any errors with the presentation.

Returns:

  • (Boolean)

    true if there any errors with the presentation.



232
233
234
# File 'lib/ruic/presentation.rb', line 232

def errors?
	(!errors.empty?)
end

#find(criteria = {}) {|asset, index| ... } ⇒ Array<MetaData::AssetBase>

Find assets in this presentation matching criteria.

Examples:

1) Searching for simple values

every_asset   = preso.find                          # Every asset in the presentation
master_assets = preso.find _master:true             # Test for master/nonmaster
models        = preso.find _type:'Model'            # …or based on type
slide2_assets = preso.find _slide:2                 # …or presence on a specific slide
rectangles    = preso.find sourcepath:'#Rectangle'  # …or attribute values
gamecovers    = preso.find name:'Game Cover'        # …including the name

2) Combine tests to get more specific

master_models = preso.find _type:'Model', _master:true
slide2_rects  = preso.find _type:'Model', _slide:2, sourcepath:'#Rectangle'
nonmaster_s2  = preso.find _slide:2, _master:false
red_materials = preso.find _type:'Material', diffuse:[1,0,0]

3) Matching values more loosely

pistons       = preso.find name:/^Piston/           # Regex for batch finding
bottom_row    = preso.find position:[nil,-200,nil]  # nil for wildcards in vectors

4) Restrict the search to a sub-tree

group        = preso/"Scene.Layer.Group"
group_models = preso.find _under:group, _type:'Model'  # All models under the group
group_models = group.find _type:'Model'                # alternatively start from the asset

5) Iterate the results as they are found

preso.find _type:'Model', name:/^Piston/ do |model, index|
   show "Model #{index} is named #{model.name}"
end

# You do not need to receive the index if you do not need its value
group.find _type:'Model' do |model|
   scale = model['scale'].value
   scale.x = scale.y = scale.z = 1
end

Parameters:

  • criteria (Hash) (defaults to: {})

    Attribute names and values, along with a few special keys.

Options Hash (criteria):

  • :_type (String)

    asset must be of specified type, e.g. "Model" or "Material" or "PathAnchorPoint".

  • :_slide (Integer, String)

    slide number or name that the asset must be present on.

  • :_master (Boolean)

    true for only master assets, false for only non-master assets.

  • :_under (MetaData::AssetBase)

    a root asset to require as an ancestor; this asset will never be in the search results.

  • :attribute_name (Numeric)

    numeric attribute value must be within 0.001 of the supplied value.

  • :attribute_name (String)

    string attribute value must match the supplied string exactly.

  • :attribute_name (Regexp)

    supplied regex must match the string attribute value.

  • :attribute_name (Array)

    each component of the attribute's vector value must be within 0.001 of the supplied value in the array; if a value in the array is nil that component of the vector may have any value.

Yields:

  • (asset, index)

    Yields each found asset (and its index in the results) to the block (if supplied) as they are found.

Returns:

See Also:



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/ruic/presentation.rb', line 510

def find(criteria={},&block)
	index = -1
	start = criteria.key?(:_under) ? criteria.delete(:_under).el : @graph
	[].tap do |result|
		start.xpath('./descendant::*').each do |el|
			asset = asset_for_el(el)
			next unless criteria.all? do |att,val|
				case att
					when :_type   then el.name == val
					when :_slide  then has_slide?(asset,val)
					when :_master then master?(asset)==val
					else
						if asset.properties[att.to_s]
							value = asset[att.to_s].value
							case val
								when Regexp  then val =~ value.to_s
								when Numeric then (val-value).abs < 0.001
								when Array   then value.to_a.zip(val).map{ |a,b| !b || (a-b).abs<0.001 }.all?
								else value == val
							end
						end
				end
			end
			yield asset, index+=1 if block_given?
			result << asset
		end
	end
end

#get_attribute(asset, attr_name, slide_name_or_index) ⇒ Object

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.

This method is used internally by assets; accessing attributes directly from the asset is generally more appropriate.

Examples:

preso = app.main_presentation
camera = preso/"Scene.Layer.Camera"

assert preso.get_attribute(camera,'position',0) == camera['position',0]

Parameters:

  • asset (MetaData::AssetBase)

    the asset to fetch the attribute for.

  • attr_name (String)

    the name of the attribute to get the value of.

  • slide_name_or_index (String, Integer)

    the string name or integer index of the slide.

Returns:

  • (Object)

    the value of the asset on the slide

See Also:



290
291
292
293
294
295
296
297
# File 'lib/ruic/presentation.rb', line 290

def get_attribute( asset, attr_name, slide_name_or_index )
	graph_element = asset.el
	((addsets=@addsets_by_graph[graph_element]) && ( # State (slide) don't have any addsets
		( addsets[slide_name_or_index] && addsets[slide_name_or_index][attr_name] ) || # Try for a Set on the specific slide
		( addsets[0] && addsets[0][attr_name] ) # …else try the master slide
	) || graph_element[attr_name]) # …else try the graph
	# TODO: handle animation (child of addset)
end

#has_slide?(asset, slide_name_or_index) ⇒ Boolean

Returns true if the asset exists on the supplied slide.

Returns:

  • (Boolean)

    true if the asset exists on the supplied slide.

See Also:



388
389
390
391
392
393
394
395
396
# File 'lib/ruic/presentation.rb', line 388

def has_slide?( asset, slide_name_or_index )
	graph_element = asset.el
	if graph_element == @scene
		# 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
		has_slide?( asset_for_el( @addsets_by_graph.first.first ), slide_name_or_index )
	else
		@addsets_by_graph[graph_element][slide_name_or_index] || @addsets_by_graph[graph_element][0]
	end
end

#image_pathsArray<String>

Returns array of all image paths referenced by this presentation.

Returns:

  • (Array<String>)

    array of all image paths referenced by this presentation.



157
158
159
# File 'lib/ruic/presentation.rb', line 157

def image_paths
	image_usage.keys
end

#image_usageHash

Returns a mapping of image paths to arrays of the assets referencing them.

Returns:

  • (Hash)

    a mapping of image paths to arrays of the assets referencing them.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ruic/presentation.rb', line 127

def image_usage
	# 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.
	asset_types = app..by_name.values + @class_by_ref.values

	image_properties_by_type = asset_types.flat_map do |type|
		type.properties.values
		    .select{ |property| property.type=='Image' || property.type == 'Texture' }
		    .map{ |property| [type,property] }
	end.group_by(&:first).tap{ |x| x.each{ |t,a| a.map!(&:last) } }

	Hash[ assets.each_with_object({}) do |asset,usage|
		if properties = image_properties_by_type[asset.class]
			properties.each do |property|
				asset[property.name].values.compact.each do |value|
					value = value['sourcepath'] if property.type=='Image'
					unless value.nil? || value.empty?
						value = value.gsub('\\','/').sub(/^.\//,'')
						usage[value] ||= []
						usage[value] << asset
					end
				end
			end
		end
	end.sort_by do |path,assets|
		parts = path.downcase.split '/'
		[ parts.length, parts ]
	end ].tap{ |h| h.extend(UIC::PresentableHash) }
end

#load_from_filenil

Load information for the presentation from disk. If you supply a path to a .uip file when creating the presentation this method is automatically called.

Returns:

  • (nil)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ruic/presentation.rb', line 17

def load_from_file
	# 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
	@doc = Nokogiri.XML( File.read( file, encoding:'utf-8' ), &:noblanks )
	@graph = @doc.at('Graph')
	@scene = @graph.at('Scene')
	@logic = @doc.at('Logic')

	@class_by_ref = {}
	@doc.xpath('/UIP/Project/Classes/*').each do |reference|
		path = app.path_to(reference['sourcepath'])
		raise "Cannot find file '#{path}' referenced by #{self.inspect}" unless File.exist?( path )
		metaklass = case reference.name
			when 'CustomMaterial'
				meta = Nokogiri.XML(File.read(path,encoding:'utf-8')).at('/*/MetaData')
				from = app..by_name[ 'MaterialBase' ]
				app..create_class( meta, from, reference.name )
			when 'Effect'
				meta = Nokogiri.XML(File.read(path,encoding:'utf-8')).at('/*/MetaData')
				from = app..by_name[ 'Effect' ]
				app..create_class( meta, from, reference.name )
			when 'Behavior'
				lua  = File.read(path,encoding:'utf-8')
				meta = lua[ /--\[\[(.+?)(?:--)?\]\]/m, 1 ]
				meta = Nokogiri.XML("<MetaData>#{meta}</MetaData>").root
				from = app..by_name[ 'Behavior' ]
				app..create_class( meta, from, reference.name )
		end
		@class_by_ref[ "##{reference['id']}" ] = metaklass
		nil
	end

	rebuild_caches_from_document

	@asset_by_el  = {} # indexed by asset graph element
	@slides_for   = {} # indexed by asset graph element
	@slides_by_el = {} # indexed by slide state element
end

#master?(asset) ⇒ Boolean

Returns true if the asset is added on the master slide.

Returns:

  • (Boolean)

    true if the asset is added on the master slide.

See Also:



453
454
455
456
# File 'lib/ruic/presentation.rb', line 453

def master?(asset)
	graph_element = asset.el
	(graph_element == @scene) || !!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][0])
end

#owning_component(asset) ⇒ MetaData::AssetBase

Returns the component (or Scene) asset that owns the supplied asset.

Returns:

See Also:



342
343
344
# File 'lib/ruic/presentation.rb', line 342

def owning_component( asset )
	asset_for_el( owning_component_element( asset.el ) )
end

#parent_asset(child_asset) ⇒ MetaData::AssetBase

Returns the scene graph parent of the child asset, or nil for the Scene.

Parameters:

Returns:

See Also:



107
108
109
110
111
112
# File 'lib/ruic/presentation.rb', line 107

def parent_asset( child_asset )
	child_graph_el = child_asset.el
	unless child_graph_el==@scene || child_graph_el.parent.nil?
		asset_for_el( child_graph_el.parent )
	end
end

#path_to(asset, from_asset = nil) ⇒ String

Generate the script path for an asset in the presentation.

  • If from_asset is supplied the path will be relative to that asset (e.g. "parent.parent.Group.Model").
  • If from_asset is omitted the path will be absolute (e.g. "Scene.Layer.Group.Model").

This is used internally by MetaData::AssetBase#path and MetaData::AssetBase#path_to; those methods are usually more convenient to use.

Examples:

preso = app.main
preso.scene.children.each{ |child| show preso.path_to(child) }
#=> "main:Scene.MyAppBehavior"
#=> "main:Scene.UILayer"
#=> "main:Scene.ContentLayer"
#=> "main:Scene.BGLayer"

Parameters:

Returns:

  • (String)

    the script path to the element.

See Also:



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/ruic/presentation.rb', line 203

def path_to( asset, from_asset=nil )
	el = asset.el

	to_parts = if el.ancestors('Graph')
		[].tap{ |parts|
			until el==@graph
				parts.unshift asset_for_el(el).name
				el = el.parent
			end
		}
	end
	if from_asset && from_asset.el.ancestors('Graph')
		from_el = from_asset.el
		from_parts = [].tap{ |parts|
			until from_el==@graph
				parts.unshift asset_for_el(from_el).name
				from_el = from_el.parent
			end
		}
		until to_parts.empty? || from_parts.empty? || to_parts.first!=from_parts.first
			to_parts.shift
			from_parts.shift
		end
		to_parts.unshift *(['parent']*from_parts.length)
	end
	to_parts.join('.')
end

#rebuild_caches_from_documentnil

Update the presentation to be in-sync with the document. Must be called whenever the in-memory representation of the XML document is changed. Called automatically by all necessary methods; only necessary if script (dangerously) manipulates the .doc of the presentation directly.

Returns:

  • (nil)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ruic/presentation.rb', line 68

def rebuild_caches_from_document
	@graph_by_id = {}
	@scene.traverse{ |x| @graph_by_id[x['id']]=x if x.is_a?(Nokogiri::XML::Element) }

	@graph_by_addset  = {}
	@addsets_by_graph = {}
	slideindex = {}
	@logic.xpath('.//Add|.//Set').each do |addset|
		graph = @graph_by_id[addset['ref'][1..-1]]
		@graph_by_addset[addset] = graph
		@addsets_by_graph[graph] ||= {}
		slide = addset.parent
		name  = slide['name']
		index = name == 'Master Slide' ? 0 : (slideindex[slide] ||= (slide.index('State') + 1))
		@addsets_by_graph[graph][name]  = addset
		@addsets_by_graph[graph][index] = addset
	end
	nil
end

#replace_asset(existing_asset, new_type, attributes = {}) ⇒ MetaData::AssetBase

Replace an existing asset with a new kind of asset.

Parameters:

  • existing_asset (MetaData::AssetBase)

    the existing asset to replace.

  • new_type (String)

    the name of the asset type, e.g. "ReferencedMaterial" or "Group".

  • attributes (Hash) (defaults to: {})

    initial attribute values for the new asset.

Returns:



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/ruic/presentation.rb', line 439

def replace_asset( existing_asset, new_type, attributes={} )
	old_el = existing_asset.el
	new_el = old_el.replace( "<#{new_type}/>" ).first
	attributes['id'] = old_el['id']
	attributes.each{ |att,val| new_el[att.to_s] = val }
	asset_for_el( new_el ).tap do |new_asset|
		unsupported_attributes = ".//*[name()='Add' or name()='Set'][@ref='##{old_el['id']}']/@*[name()!='ref' and #{new_asset.properties.keys.map{|p| "name()!='#{p}'"}.join(' and ')}]"
		@logic.xpath(unsupported_attributes).remove
		rebuild_caches_from_document
	end
end

#sceneMetaData::Scene

Returns the root scene asset for the presentation.

Returns:

  • (MetaData::Scene)

    the root scene asset for the presentation.



177
178
179
# File 'lib/ruic/presentation.rb', line 177

def scene
	asset_for_el( @scene )
end

#set_attribute(asset, property_name, slide_name_or_index, str) ⇒ Object

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.

This method is used internally by assets; accessing attributes directly from the asset is generally more appropriate.

Examples:

preso = app.main_presentation
camera = preso/"Scene.Layer.Camera"

# The long way to set the attribute value
preso.set_attribute(camera,'endtime',0,1000)

# …and the shorter way
camera['endtime',0] = 1000

Parameters:

  • asset (MetaData::AssetBase)

    the asset to fetch the attribute for.

  • attr_name (String)

    the name of the attribute to get the value of.

  • slide_name_or_index (String, Integer)

    the string name or integer index of the slide.

See Also:



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ruic/presentation.rb', line 317

def set_attribute( asset, property_name, slide_name_or_index, str )
	graph_element = asset.el
	if attribute_linked?( asset, property_name )
		if @addsets_by_graph[graph_element]
			@addsets_by_graph[graph_element][0][property_name] = str
		else
			raise "TODO"
		end
	else
		if @addsets_by_graph[graph_element]
			if slide_name_or_index
				@addsets_by_graph[graph_element][slide_name_or_index][property_name] = str
			else
				master = master_slide_for( graph_element )
				slide_count = master.xpath('count(./State)').to_i
				0.upto(slide_count).each{ |idx| set_attribute(asset,property_name,idx,str) }
			end
		else
			raise "TODO"
		end
	end
end

#slide_index(asset) ⇒ Integer

Returns the index of the first slide where an asset is added (0 for master, non-zero for non-master).

Parameters:

Returns:

  • (Integer)

    the index of the first slide where an asset is added (0 for master, non-zero for non-master).



97
98
99
100
101
102
# File 'lib/ruic/presentation.rb', line 97

def slide_index(asset)
	# TODO: probably faster to .find the first @addsets_by_graph
	id = asset.el['id']
	slide = @logic.at(".//Add[@ref='##{id}']/..")
	(slide ? slide.xpath('count(ancestor::State) + count(preceding-sibling::State[ancestor::State])').to_i : 0) # the Scene is never added
end

#slides_for(asset) ⇒ SlideCollection

Returns an array-like collection of all slides that the asset is available on.

Parameters:

Returns:

  • (SlideCollection)

    an array-like collection of all slides that the asset is available on.

See Also:



374
375
376
377
378
379
380
381
382
383
384
# File 'lib/ruic/presentation.rb', line 374

def slides_for( asset )
	graph_element = asset.el
	@slides_for[graph_element] ||= begin
		slides = []
		master = master_slide_for( graph_element )
		slides << [master,0] if graph_element==@scene || (@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][0])
		slides.concat( master.xpath('./State').map.with_index{ |el,i| [el,i+1] } )
		slides.map!{ |el,idx| @slides_by_el[el] ||= app..new_instance(self,el).tap{ |s| s.index=idx; s.name=el['name'] } }
		UIC::SlideCollection.new( slides )
	end
end

#to_xmlString

Returns the xml representation of this presentation. Formatted to match UI Composer Studio's formatting as closely as possible (for minimal diffs after update).

Returns:

  • (String)

    the xml representation of this presentation. Formatted to match UI Composer Studio's formatting as closely as possible (for minimal diffs after update).



56
57
58
59
60
# File 'lib/ruic/presentation.rb', line 56

def to_xml
	doc.to_xml( indent:1, indent_text:"\t" )
	   .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
	   .sub('"?>','" ?>')
end

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.

Parameters:

  • asset (MetaData::AssetBase)

    the master asset to unlink the attribute on.

  • attribute_name (String)

    the name of the attribute to unlink.

Returns:

  • (Boolean)

    true if the attribute was previously linked; false otherwise.



418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/ruic/presentation.rb', line 418

def unlink_attribute(asset,attribute_name)
	graph_element = asset.el
	if master?(asset) && attribute_linked?(asset,attribute_name)
		master_value = get_attribute( asset, attribute_name, 0 )
		slides_for( asset ).to_ary[1..-1].each do |slide|
			addset = slide.el.at_xpath( ".//*[@ref='##{graph_element['id']}']" ) || slide.el.add_child("<Set ref='##{graph_element['id']}'/>").first
			addset[attribute_name] = master_value
		end
		rebuild_caches_from_document
		true
	else
		false
	end
end