class UIC::Presentation
include UIC::FileBacked
def initialize( uip_path )
self.file = uip_path
load_from_file if file_found?
end
def load_from_file
@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.metadata.by_name[ 'MaterialBase' ]
app.metadata.create_class( meta, from, reference.name )
when 'Effect'
meta = Nokogiri.XML(File.read(path,encoding:'utf-8')).at('/*/MetaData')
from = app.metadata.by_name[ 'Effect' ]
app.metadata.create_class( meta, from, reference.name )
when 'Behavior'
lua = File.read(path,encoding:'utf-8')
meta = lua[ /--\[\[(.+?)(?:--)?\]\]/m, 1 ]
meta = Nokogiri.XML("#{meta}").root
from = app.metadata.by_name[ 'Behavior' ]
app.metadata.create_class( meta, from, reference.name )
end
@class_by_ref[ "##{reference['id']}" ] = metaklass
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
def save!
File.open(file,'w:utf-8'){ |f|
f << @doc.to_xml( indent:1, indent_text:"\t" )
.gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
.sub('"?>','" ?>')
}
end
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
end
def asset_by_id( id )
(@graph_by_id[id] && asset_for_el( @graph_by_id[id] ))
end
# Find the index of the slide where an element is added
def slide_index(graph_element)
# TODO: probably faster to .find the first @addsets_by_graph
slide = @logic.at(".//Add[@ref='##{graph_element['id']}']/..")
(slide ? slide.xpath('count(ancestor::State) + count(preceding-sibling::State[ancestor::State])').to_i : 0) # the Scene is never added
end
def parent_asset( child_graph_el )
unless child_graph_el==@scene || child_graph_el.parent.nil?
asset_for_el( child_graph_el.parent )
end
end
def child_assets( parent_graph_el )
parent_graph_el.element_children.map{ |child| asset_for_el(child) }
end
# Get an array of all assets in the scene graph, in document order
def assets
@graph_by_id.map{ |id,graph_element| asset_for_el(graph_element) }
end
# Returns a hash mapping image paths to arrays of the assets referencing them
def image_usage
asset_types = app.metadata.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
def image_paths
image_usage.keys
end
def asset_for_el(el)
(@asset_by_el[el] ||= el['class'] ? @class_by_ref[el['class']].new(self,el) : app.metadata.new_instance(self,el))
end
attr_reader :addsets_by_graph
protected :addsets_by_graph
def referenced_files
(
(images + behaviors + effects + meshes + materials ).map(&:file)
+ effects.flat_map(&:images)
+ fonts
).sort_by{ |f| parts = f.split(/[\/\\]/); [parts.length,parts] }
end
def scene
asset_for_el( @scene )
end
def path_to( el, from_el=nil )
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_el && from_el.ancestors('Graph')
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
def errors?
(!errors.empty?)
end
def errors
(file_found? ? [] : ["File not found: '#{file}'"])
end
def at(path,root=@graph)
name,path = path.split('.',2)
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
alias_method :/, :at
def get_attribute( graph_element, property_name, slide_name_or_index )
((addsets=@addsets_by_graph[graph_element]) && ( # State (slide) don't have any addsets
( addsets[slide_name_or_index] && addsets[slide_name_or_index][property_name] ) || # Try for a Set on the specific slide
( addsets[0] && addsets[0][property_name] ) # …else try the master slide
) || graph_element[property_name]) # …else try the graph
# TODO: handle animation (child of addset)
end
def set_attribute( graph_element, property_name, slide_name_or_index, str )
if attribute_linked?( graph_element, 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(graph_element,property_name,idx,str) }
end
else
raise "TODO"
end
end
end
def owning_component( graph_element )
asset_for_el( owning_component_element( graph_element ) )
end
def owning_component_element( graph_element )
graph_element.at_xpath('(ancestor::Component[1] | ancestor::Scene[1])[last()]')
end
def owning_or_self_component_element( graph_element )
graph_element.at_xpath('(ancestor-or-self::Component[1] | ancestor-or-self::Scene[1])[last()]')
end
def master_slide_for( graph_element )
comp = owning_or_self_component_element( graph_element )
@logic.at("./State[@component='##{comp['id']}']")
end
def slides_for( graph_element )
@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.metadata.new_instance(self,el).tap{ |s| s.index=idx; s.name=el['name'] } }
UIC::SlideCollection.new( slides )
end
end
def has_slide?( graph_element, slide_name_or_index )
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?( @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
def attribute_linked?(graph_element,attribute_name)
!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][1] && @addsets_by_graph[graph_element][1].key?(attribute_name))
end
def unlink_attribute(graph_element,attribute_name)
if master?(graph_element) && attribute_linked?(graph_element,attribute_name)
master_value = get_attribute( graph_element, attribute_name, 0 )
slides_for( graph_element ).to_ary[1..-1].each do |slide|
addset = slide.el.at_xpath( ".//*[@ref='##{graph_element['id']}']" ) || slide.el.add_child("").first
addset[attribute_name] = master_value
end
rebuild_caches_from_document
true
end
end
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
# Is this element added on the master slide?
def master?(graph_element)
(graph_element == @scene) || !!(@addsets_by_graph[graph_element] && @addsets_by_graph[graph_element][0])
end
def find(options={})
index = -1
start = options[:under] ? options[:under].el : @graph
(options[:attributes]||={})[:name]=options[:name] if options[:name]
[].tap do |result|
start.xpath('./descendant::*').each do |el|
next if options.key?(:type) && el.name != options[:type]
next if options.key?(:slide) && !has_slide?(el,options[:slide])
next if options.key?(:master) && master?(el)!= options[:master]
asset = asset_for_el(el)
next if options.key?(:attributes) && options[:attributes].any?{ |att,val|
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 }.any?
else value != val
end if asset.properties[att.to_s]
}
yield asset, index+=1 if block_given?
result << asset
end
end
end
end
def UIC.Presentation( uip_path )
UIC::Presentation.new( uip_path )
end
class UIC::Application::Presentation < UIC::Presentation
include UIC::ElementBacked
xmlattribute :id
xmlattribute :src
xmlattribute :id do |new_id|
main_preso = app.main_presentation
super(new_id)
app.main_presentation=self if main_preso==self
end
xmlattribute :active
def initialize(application,el)
self.owner = application
self.el = el
super( application.path_to(src) )
end
alias_method :app, :owner
def path_to( el, from=nil )
"#{id}:#{super}"
end
def inspect
""
end
end
class Nokogiri::XML::Element
def index(kind='*') # Find the index of this element amongs its siblings
xpath("count(./preceding-sibling::#{kind})").to_i
end
end