RUIC 0.6.0 → 0.6.1

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -3
  3. data/HISTORY +110 -86
  4. data/README.md +220 -220
  5. data/bin/ruic +61 -61
  6. data/gui/TODO +2 -2
  7. data/gui/appattributesmodel.rb +51 -51
  8. data/gui/appelementsmodel.rb +126 -126
  9. data/gui/launch.rb +19 -19
  10. data/gui/makefile +14 -14
  11. data/gui/resources/style/dark.qss +459 -459
  12. data/gui/window.rb +90 -90
  13. data/gui/window.ui +753 -753
  14. data/lib/ruic.rb +191 -190
  15. data/lib/ruic/application.rb +25 -3
  16. data/lib/ruic/assets.rb +441 -436
  17. data/lib/ruic/attributes.rb +179 -178
  18. data/lib/ruic/behaviors.rb +0 -0
  19. data/lib/ruic/effect.rb +31 -31
  20. data/lib/ruic/interfaces.rb +0 -0
  21. data/lib/ruic/nicebytes.rb +29 -0
  22. data/lib/ruic/presentation.rb +6 -3
  23. data/lib/ruic/renderplugin.rb +17 -17
  24. data/lib/ruic/ripl.rb +45 -45
  25. data/lib/ruic/statemachine.rb +6 -0
  26. data/lib/ruic/version.rb +3 -3
  27. data/ruic.gemspec +25 -25
  28. data/test/MetaData-simple.xml +28 -28
  29. data/test/MetaData.xml +435 -435
  30. data/test/customclasses.ruic +29 -29
  31. data/test/filtering.ruic +42 -42
  32. data/test/futureassets.ruic +7 -7
  33. data/test/nonmaster.ruic +20 -20
  34. data/test/paths.ruic +16 -16
  35. data/test/projects/CustomClasses/CustomClasses.uia +7 -7
  36. data/test/projects/CustomClasses/CustomClasses.uip +40 -40
  37. data/test/projects/CustomClasses/FutureAsset.uip +17 -17
  38. data/test/projects/MissingAssets/Existing.uip +87 -87
  39. data/test/projects/MissingAssets/MissingAssets.uia +0 -0
  40. data/test/projects/MissingAssets/MissingAssets.uia-user +14 -14
  41. data/test/projects/MissingAssets/RoundedPlane-1/RoundedPlane-1.import +18 -18
  42. data/test/projects/MissingAssets/RoundedPlane-1/meshes/RoundedPlane.mesh +0 -0
  43. data/test/projects/MissingAssets/effects/effects.txt +0 -0
  44. data/test/projects/MissingAssets/effects/existing.effect +0 -0
  45. data/test/projects/MissingAssets/fonts/Arimo-Regular.ttf +0 -0
  46. data/test/projects/MissingAssets/fonts/Chivo-Black.ttf +0 -0
  47. data/test/projects/MissingAssets/fonts/OFL.txt +0 -0
  48. data/test/projects/MissingAssets/maps/effects/brushnoise.dds +0 -0
  49. data/test/projects/MissingAssets/maps/existing.png +0 -0
  50. data/test/projects/MissingAssets/maps/maps.txt +0 -0
  51. data/test/projects/MissingAssets/maps/materials/concrete_plain.png +0 -0
  52. data/test/projects/MissingAssets/maps/materials/concrete_plain_bump.png +0 -0
  53. data/test/projects/MissingAssets/maps/materials/spherical_checker.png +0 -0
  54. data/test/projects/MissingAssets/materials/concrete.material +0 -0
  55. data/test/projects/MissingAssets/materials/materials.txt +0 -0
  56. data/test/projects/MissingAssets/scripts/existing1.lua +0 -0
  57. data/test/projects/MissingAssets/scripts/existing2.lua +0 -0
  58. data/test/projects/MissingAssets/states/existing.scxml +0 -0
  59. data/test/projects/MissingAssets/tests/interface-navigation.scxml-test +0 -0
  60. data/test/projects/Paths/Paths.uia +4 -4
  61. data/test/projects/Paths/Paths.uip +98 -98
  62. data/test/projects/SimpleScene/SimpleScene.uia +4 -4
  63. data/test/projects/SimpleScene/SimpleScene.uip +35 -35
  64. data/test/properties.ruic +80 -80
  65. data/test/referencematerials.ruic +50 -50
  66. data/test/usage.ruic +54 -54
  67. metadata +5 -54
@@ -1,191 +1,192 @@
1
- #encoding:utf-8
2
-
3
- class RUIC; end
4
- module UIC; end
5
-
6
- require 'nokogiri'
7
- require_relative 'ruic/version'
8
- require_relative 'ruic/attributes'
9
- require_relative 'ruic/assets'
10
- require_relative 'ruic/interfaces'
11
- require_relative 'ruic/application'
12
- require_relative 'ruic/behaviors'
13
- require_relative 'ruic/effect'
14
- require_relative 'ruic/renderplugin'
15
- require_relative 'ruic/statemachine'
16
- require_relative 'ruic/presentation'
17
- require_relative 'ruic/ripl'
18
-
19
- # The `RUIC` class provides the interface for running scripts using the special DSL,
20
- # and for running the interactive REPL.
21
- # See the {file:README.md README} file for description of the DSL.
22
- class RUIC
23
- DEFAULTMETADATA = 'C:/Program Files (x86)/NVIDIA Corporation/UI Composer 8.0/res/DataModelMetadata/en-us/MetaData.xml'
24
-
25
- # Execute a script and/or launch the interactive REPL.
26
- #
27
- # If you both run a `:script` and then enter the `:repl` all local variables created
28
- # by the script will be available in the REPL.
29
- #
30
- # # Just run a script
31
- # RUIC.run script:'my.ruic'
32
- #
33
- # # Load an application and then enter the REPL
34
- # RUIC.run uia:'my.uia', repl:true
35
- #
36
- # # Run a script and then drop into the REPL
37
- # RUIC.run script:'my.ruic', repl:true
38
- #
39
- # The working directory for scripts is set to the directory of the script.
40
- #
41
- # The working directory for the repl is set to the directory of the first
42
- # loaded application (if any);
43
- # failing that, the directory of the script (if any);
44
- # failing that, the current directory.
45
- #
46
- # @option opts [String] :script A path to a `.ruic` script to run _(optional)_.
47
- # @option opts [String] :uia An `.uia` file to load (before running the script and/or REPL) _(optional)_.
48
- # @option opts [Boolean] :repl Pass `true` to enter the command-line REPL after executing the script (if any).
49
- # @return [nil]
50
- def self.run(opts={})
51
- opts = opts
52
- ruic = nil
53
- if opts[:script]
54
- script = File.read(opts[:script],encoding:'utf-8')
55
- Dir.chdir(File.dirname(opts[:script])) do
56
- ruic = self.new
57
- ruic.metadata opts[:metadata] if opts[:metadata]
58
- ruic.uia opts[:uia] if opts[:uia]
59
- ruic.env.eval(script,opts[:script])
60
- end
61
- end
62
-
63
- if opts[:repl]
64
- location = (ruic && ruic.app && ruic.app.respond_to?(:file) && ruic.app.file) || opts[:uia] || opts[:script] || '.'
65
- Dir.chdir( File.dirname(location) ) do
66
- ruic ||= self.new.tap do |r|
67
- r.metadata opts[:metadata] if opts[:metadata]
68
- r.uia opts[:uia] if opts[:uia]
69
- end
70
- require 'ripl/irb'
71
- require 'ripl/multi_line'
72
- require 'ripl/multi_line/live_error.rb'
73
- Ripl::MultiLine.engine = Ripl::MultiLine::LiveError
74
- Ripl::Shell.include Ripl::MultiLine.engine
75
- Ripl::Shell.include Ripl::AfterResult
76
- Ripl::Shell.include Ripl::FormatResult
77
- Ripl.config.merge! prompt:"", result_prompt:'#=> ', multi_line_prompt:' ', irb_verbose:false, after_result:"\n", result_line_limit:120, prefix_result_lines:true, skip_nil_results:true
78
- ARGV.clear # So that RIPL doesn't try to interpret the options
79
- puts "(RUIC v#{RUIC::VERSION} interactive session; 'quit' or ctrl-d to end)"
80
- ruic.instance_eval{ puts @apps.map{ |n,app| "(#{n} is #{app.inspect})" } }
81
- puts "" # blank line before first input
82
- Ripl.start binding:ruic.env
83
- end
84
- end
85
- end
86
-
87
- # Creates a new environment for executing a RUIC script.
88
- # @param metadata [String] Path to the `MetaData.xml` file to use.
89
- def initialize( metadata=DEFAULTMETADATA )
90
- @metadata = metadata
91
- @apps = {}
92
- end
93
-
94
- # Set the metadata to use; generally called from the RUIC DSL.
95
- # @param path [String] Path to the `MetaData.xml` file, either absolute or relative to the working directory.
96
- def metadata(path)
97
- @metadata = path
98
- end
99
-
100
- # Load an application, making it available as `app`, `app2`, etc.
101
- # @param path [String] Path to the `*.uia` application file.
102
- # @return [UIC::Application] The new application loaded.
103
- def uia(path)
104
- meta = UIC.MetaData @metadata
105
- name = @apps.empty? ? :app : :"app#{@apps.length+1}"
106
- @apps[name] = UIC.Application(meta,path)
107
- end
108
-
109
- # @return [Binding] the shared binding used for evaluating the script and REPL
110
- def env
111
- @env ||= binding
112
- end
113
-
114
- # @private used as a one-off
115
- module SelfInspecting; def inspect; to_s; end; end
116
-
117
- # Used to resolve bare `app` and `app2` calls to a loaded {UIC::Application Application}.
118
- # @return [UIC::Application] the new application loaded.
119
- def method_missing(name,*a)
120
- @apps[name] || (name=~/^app\d*/ ? "(no #{name} loaded)".extend(SelfInspecting) : super)
121
- end
122
-
123
- # Simple assertion mechanism to be used within scripts.
124
- #
125
- # @example 1) simple call syntax
126
- # # Provides a generic failure message
127
- # assert a==b
128
- # #=> assertion failed (my.ruic line 17)
129
- #
130
- # # Provides a custom failure message
131
- # assert a==b, "a should equal b"
132
- # #=> a should equal b : assertion failed (my.ruic line 17)
133
- #
134
- # @example 2) block with string syntax
135
- # # The code in the string to eval is also the failure message
136
- # assert{ "a==b" }
137
- # #=> a==b : assertion failed (my.ruic line 17)
138
- #
139
- # @param condition [Boolean] the value to evaluate.
140
- # @param msg [String] the nice error message to display.
141
- # @yieldreturn [String] the code to evaluate as a condition.
142
- def assert(condition=:CONDITIONNOTSUPPLIED,msg=nil,&block)
143
- if block && condition==:CONDITIONNOTSUPPLIED
144
- msg = yield
145
- condition = msg.is_a?(String) ? eval(msg,block.binding) : msg
146
- end
147
- condition || begin
148
- file, line, _ = caller.first.split(':')
149
- puts "#{"#{msg} : " unless msg.nil?}assertion failed (#{file} line #{line})"
150
- exit 1
151
- end
152
- end
153
-
154
- # Nicer name for `puts` to be used in the DSL, printing the
155
- # 'nice' string equivalent for all supplied arguments.
156
- def show(*a)
157
- a=a.first if a.length==1 && a.first.is_a?(Array)
158
- opts = { result_prompt:'# ', result_line_limit:120, prefix_result_lines:true, to_s:true }
159
- a.each{ |x| puts Ripl::FormatResult.format_result(x,opts) }
160
- nil # so that Ripl won't show the result
161
- end
162
-
163
- def inspect
164
- "<RUIC #{@apps.empty? ? "(no app loaded)" : Hash[ @apps.map{ |id,app| [id,File.basename(app.file)] } ]}>"
165
- end
166
- end
167
-
168
- # Run a series of commands inside the RUIC DSL.
169
- #
170
- # @example
171
- # require 'ruic'
172
- # RUIC do
173
- # uia 'test/MyProject/MyProject.uia'
174
- # show app
175
- # #=>UIC::Application 'MyProject.uia'>
176
- # end
177
- #
178
- # If no block is supplied, this is the same as {RUIC.run RUIC.run(opts)}.
179
- # @option opts [String] :uia Optionally load an application before running the script.
180
- def RUIC(opts={},&block)
181
- if block
182
- Dir.chdir(File.dirname($0)) do
183
- RUIC.new.tap do |r|
184
- r.metadata opts[:metadata] if opts[:metadata]
185
- r.uia opts[:uia] if opts[:uia]
186
- end.instance_eval(&block)
187
- end
188
- else
189
- RUIC.run(opts)
190
- end
1
+ #encoding:utf-8
2
+
3
+ class RUIC; end
4
+ module UIC; end
5
+
6
+ require 'nokogiri'
7
+ require_relative 'ruic/version'
8
+ require_relative 'ruic/attributes'
9
+ require_relative 'ruic/assets'
10
+ require_relative 'ruic/interfaces'
11
+ require_relative 'ruic/application'
12
+ require_relative 'ruic/behaviors'
13
+ require_relative 'ruic/effect'
14
+ require_relative 'ruic/renderplugin'
15
+ require_relative 'ruic/statemachine'
16
+ require_relative 'ruic/presentation'
17
+ require_relative 'ruic/ripl'
18
+ require_relative 'ruic/nicebytes'
19
+
20
+ # The `RUIC` class provides the interface for running scripts using the special DSL,
21
+ # and for running the interactive REPL.
22
+ # See the {file:README.md README} file for description of the DSL.
23
+ class RUIC
24
+ DEFAULTMETADATA = 'C:/Program Files (x86)/NVIDIA Corporation/UI Composer 8.0/res/DataModelMetadata/en-us/MetaData.xml'
25
+
26
+ # Execute a script and/or launch the interactive REPL.
27
+ #
28
+ # If you both run a `:script` and then enter the `:repl` all local variables created
29
+ # by the script will be available in the REPL.
30
+ #
31
+ # # Just run a script
32
+ # RUIC.run script:'my.ruic'
33
+ #
34
+ # # Load an application and then enter the REPL
35
+ # RUIC.run uia:'my.uia', repl:true
36
+ #
37
+ # # Run a script and then drop into the REPL
38
+ # RUIC.run script:'my.ruic', repl:true
39
+ #
40
+ # The working directory for scripts is set to the directory of the script.
41
+ #
42
+ # The working directory for the repl is set to the directory of the first
43
+ # loaded application (if any);
44
+ # failing that, the directory of the script (if any);
45
+ # failing that, the current directory.
46
+ #
47
+ # @option opts [String] :script A path to a `.ruic` script to run _(optional)_.
48
+ # @option opts [String] :uia An `.uia` file to load (before running the script and/or REPL) _(optional)_.
49
+ # @option opts [Boolean] :repl Pass `true` to enter the command-line REPL after executing the script (if any).
50
+ # @return [nil]
51
+ def self.run(opts={})
52
+ opts = opts
53
+ ruic = nil
54
+ if opts[:script]
55
+ script = File.read(opts[:script],encoding:'utf-8')
56
+ Dir.chdir(File.dirname(opts[:script])) do
57
+ ruic = self.new
58
+ ruic.metadata opts[:metadata] if opts[:metadata]
59
+ ruic.uia opts[:uia] if opts[:uia]
60
+ ruic.env.eval(script,opts[:script])
61
+ end
62
+ end
63
+
64
+ if opts[:repl]
65
+ location = (ruic && ruic.app && ruic.app.respond_to?(:file) && ruic.app.file) || opts[:uia] || opts[:script] || '.'
66
+ Dir.chdir( File.dirname(location) ) do
67
+ ruic ||= self.new.tap do |r|
68
+ r.metadata opts[:metadata] if opts[:metadata]
69
+ r.uia opts[:uia] if opts[:uia]
70
+ end
71
+ require 'ripl/irb'
72
+ require 'ripl/multi_line'
73
+ require 'ripl/multi_line/live_error.rb'
74
+ Ripl::MultiLine.engine = Ripl::MultiLine::LiveError
75
+ Ripl::Shell.include Ripl::MultiLine.engine
76
+ Ripl::Shell.include Ripl::AfterResult
77
+ Ripl::Shell.include Ripl::FormatResult
78
+ Ripl.config.merge! prompt:"", result_prompt:'#=> ', multi_line_prompt:' ', irb_verbose:false, after_result:"\n", result_line_limit:200, prefix_result_lines:true, skip_nil_results:true
79
+ ARGV.clear # So that RIPL doesn't try to interpret the options
80
+ puts "(RUIC v#{RUIC::VERSION} interactive session; 'quit' or ctrl-d to end)"
81
+ ruic.instance_eval{ puts @apps.map{ |n,app| "(#{n} is #{app.inspect})" } }
82
+ puts "" # blank line before first input
83
+ Ripl.start binding:ruic.env
84
+ end
85
+ end
86
+ end
87
+
88
+ # Creates a new environment for executing a RUIC script.
89
+ # @param metadata [String] Path to the `MetaData.xml` file to use.
90
+ def initialize( metadata=DEFAULTMETADATA )
91
+ @metadata = metadata
92
+ @apps = {}
93
+ end
94
+
95
+ # Set the metadata to use; generally called from the RUIC DSL.
96
+ # @param path [String] Path to the `MetaData.xml` file, either absolute or relative to the working directory.
97
+ def metadata(path)
98
+ @metadata = path
99
+ end
100
+
101
+ # Load an application, making it available as `app`, `app2`, etc.
102
+ # @param path [String] Path to the `*.uia` application file.
103
+ # @return [UIC::Application] The new application loaded.
104
+ def uia(path)
105
+ meta = UIC.MetaData @metadata
106
+ name = @apps.empty? ? :app : :"app#{@apps.length+1}"
107
+ @apps[name] = UIC.Application(meta,path)
108
+ end
109
+
110
+ # @return [Binding] the shared binding used for evaluating the script and REPL
111
+ def env
112
+ @env ||= binding
113
+ end
114
+
115
+ # @private used as a one-off
116
+ module SelfInspecting; def inspect; to_s; end; end
117
+
118
+ # Used to resolve bare `app` and `app2` calls to a loaded {UIC::Application Application}.
119
+ # @return [UIC::Application] the new application loaded.
120
+ def method_missing(name,*a)
121
+ @apps[name] || (name=~/^app\d*/ ? "(no #{name} loaded)".extend(SelfInspecting) : super)
122
+ end
123
+
124
+ # Simple assertion mechanism to be used within scripts.
125
+ #
126
+ # @example 1) simple call syntax
127
+ # # Provides a generic failure message
128
+ # assert a==b
129
+ # #=> assertion failed (my.ruic line 17)
130
+ #
131
+ # # Provides a custom failure message
132
+ # assert a==b, "a should equal b"
133
+ # #=> a should equal b : assertion failed (my.ruic line 17)
134
+ #
135
+ # @example 2) block with string syntax
136
+ # # The code in the string to eval is also the failure message
137
+ # assert{ "a==b" }
138
+ # #=> a==b : assertion failed (my.ruic line 17)
139
+ #
140
+ # @param condition [Boolean] the value to evaluate.
141
+ # @param msg [String] the nice error message to display.
142
+ # @yieldreturn [String] the code to evaluate as a condition.
143
+ def assert(condition=:CONDITIONNOTSUPPLIED,msg=nil,&block)
144
+ if block && condition==:CONDITIONNOTSUPPLIED
145
+ msg = yield
146
+ condition = msg.is_a?(String) ? eval(msg,block.binding) : msg
147
+ end
148
+ condition || begin
149
+ file, line, _ = caller.first.split(':')
150
+ puts "#{"#{msg} : " unless msg.nil?}assertion failed (#{file} line #{line})"
151
+ exit 1
152
+ end
153
+ end
154
+
155
+ # Nicer name for `puts` to be used in the DSL, printing the
156
+ # 'nice' string equivalent for all supplied arguments.
157
+ def show(*a)
158
+ a=a.first if a.length==1 && a.first.is_a?(Array)
159
+ opts = { result_prompt:'# ', result_line_limit:200, prefix_result_lines:true, to_s:true }
160
+ a.each{ |x| puts Ripl::FormatResult.format_result(x,opts) }
161
+ nil # so that Ripl won't show the result
162
+ end
163
+
164
+ def inspect
165
+ "<RUIC #{@apps.empty? ? "(no app loaded)" : Hash[ @apps.map{ |id,app| [id,File.basename(app.file)] } ]}>"
166
+ end
167
+ end
168
+
169
+ # Run a series of commands inside the RUIC DSL.
170
+ #
171
+ # @example
172
+ # require 'ruic'
173
+ # RUIC do
174
+ # uia 'test/MyProject/MyProject.uia'
175
+ # show app
176
+ # #=>UIC::Application 'MyProject.uia'>
177
+ # end
178
+ #
179
+ # If no block is supplied, this is the same as {RUIC.run RUIC.run(opts)}.
180
+ # @option opts [String] :uia Optionally load an application before running the script.
181
+ def RUIC(opts={},&block)
182
+ if block
183
+ Dir.chdir(File.dirname($0)) do
184
+ RUIC.new.tap do |r|
185
+ r.metadata opts[:metadata] if opts[:metadata]
186
+ r.uia opts[:uia] if opts[:uia]
187
+ end.instance_eval(&block)
188
+ end
189
+ else
190
+ RUIC.run(opts)
191
+ end
191
192
  end
@@ -60,8 +60,31 @@ class UIC::Application
60
60
  # Files in the application directory not used by the application.
61
61
  #
62
62
  # @return [Array<String>] absolute paths of files in the directory not used by the application.
63
- def unused_files
64
- (directory_files - referenced_files).sort
63
+ def unused_files( hierarchy=false )
64
+ unused = (directory_files - referenced_files).sort
65
+ if hierarchy
66
+ root = File.dirname(file)
67
+ UIC.tree_hierarchy(root) do |dir|
68
+ File.directory?(dir) ? Dir.chdir(dir){ Dir['*'].map{ |f| File.expand_path(f) } } : []
69
+ end.map do |prefix,file|
70
+ if file
71
+ all = unused.select{ |path| path[/^#{file}/] }
72
+ unless all.empty?
73
+ size = NiceBytes.nice_bytes(all.map{ |f| File.size(f) }.inject(:+))
74
+ partial = file.sub(/^#{root}\//o,'')
75
+ if File.directory?(file)
76
+ "%s %s (%d files, %s)" % [prefix,partial,all.length,size]
77
+ else
78
+ "%s %s (%s)" % [prefix,partial,size]
79
+ end
80
+ end
81
+ else
82
+ prefix
83
+ end
84
+ end.compact.join("\n")
85
+ else
86
+ unused
87
+ end
65
88
  end
66
89
 
67
90
  # Files referenced by the application but not present in the directory.
@@ -71,7 +94,6 @@ class UIC::Application
71
94
  (referenced_files - directory_files).sort
72
95
  end
73
96
 
74
-
75
97
  # @return [Array<String>] absolute paths of files referenced by the application.
76
98
  def referenced_files
77
99
  # TODO: state machines can reference external scripts
@@ -1,437 +1,442 @@
1
- #encoding: utf-8
2
- class UIC::MetaData
3
-
4
- # The base class for all assets. All other classes are dynamically created when a `MetaData.xml` file is loaded.
5
- class AssetBase
6
- @properties = {}
7
- @name = "AssetBase"
8
-
9
- class << self
10
- # @return [String] The scene graph name of the asset.
11
- attr_reader :name
12
-
13
- # @return [Hash] a hash mapping attribute names to {Property} instances.
14
- def properties
15
- (ancestors[1].respond_to?(:properties) ? ancestors[1].properties : {}).merge(@properties)
16
- end
17
-
18
- # @private
19
- def inspect
20
- "<#{@name}>"
21
- end
22
- end
23
-
24
- # @return [Hash] a hash mapping attribute names to {Property} instances.
25
- def properties
26
- self.class.properties
27
- end
28
-
29
- # Find an asset by relative scripting path.
30
- #
31
- # @example
32
- # preso = app.main
33
- # layer = preso/"Scene.Layer"
34
- # cam1 = app/"main:Scene.Layer.Camera"
35
- # cam2 = preso/"Scene.Layer.Camera"
36
- # cam3 = layer/"Camera"
37
- # cam4 = cam1/"parent.Camera"
38
- #
39
- # assert cam1==cam2 && cam2==cam3 && cam3==cam4
40
- #
41
- # @return [MetaData::AssetBase] The found asset, or `nil` if it cannot be found.
42
- #
43
- # @see Application#at
44
- # @see Presentation#at
45
- def at(sub_path)
46
- presentation.at(sub_path,@el)
47
- end
48
- alias_method :/, :at
49
-
50
- # @return [Presentation] the presentation that this asset is part of.
51
- attr_accessor :presentation
52
-
53
- # @return [Nokogiri::XML::Element] the internal XML element in the scene graph of the presentation.
54
- attr_accessor :el
55
-
56
- # Create a new asset. This is called for you automatically; you likely should not be using it.
57
- # @param presentation [Presentation] the presentation owning this asset.
58
- # @param element [Nokogiri::XML::Element] the internal XML element in the scene graph of the presentation.
59
- def initialize( presentation, element )
60
- @presentation = presentation
61
- @el = element
62
- end
63
-
64
- # @return [String] the type of this asset. For example: `"Model"`, `"Material"`, `"ReferencedMaterial"`, `"PathAnchorPoint"`, etc.
65
- def type
66
- self.class.name
67
- end
68
-
69
- # @return [AssetBase] the parent of this asset in the scene graph.
70
- # @see Presentation#parent_asset
71
- def parent
72
- presentation.parent_asset(self)
73
- end
74
-
75
- # @return [Array<AssetBase>] array of child assets in the scene graph. Children are in scene graph order.
76
- # @see Presentation#child_assets
77
- def children
78
- presentation.child_assets(self)
79
- end
80
-
81
- # @return [String] the hierarchy under the element (for debugging purposes).
82
- def hierarchy
83
- presentation.hierarchy(self)
84
- end
85
-
86
- # Find descendant assets matching criteria.
87
- # This method is the same as (but more convenient than) {Presentation#find} using the `:_under` criteria.
88
- # See that method for documentation of the `criteria`.
89
- #
90
- # @example
91
- # preso = app.main
92
- # group = preso/"Scene.Layer.Vehicle"
93
- # tires = group.find name:/^Tire/
94
- #
95
- # # alternative
96
- # tires = preso.find name:/^Tire/, _under:group
97
- #
98
- # @return [Array<AssetBase>]
99
- #
100
- # @see Presentation#find
101
- def find(criteria={},&block)
102
- criteria[:_under] ||= self
103
- presentation.find(criteria,&block)
104
- end
105
-
106
- # @return [AssetBase] the component or scene that owns this asset.
107
- # If this asset is a component, does not return itself.
108
- # @see Presentation#owning_component
109
- def component
110
- presentation.owning_component(self)
111
- end
112
-
113
- # @return [Boolean] `true` if this asset is a component or Scene.
114
- def component?
115
- @el.name=='Component' || @el.name=='Scene'
116
- end
117
-
118
- # @return [Boolean] `true` if this asset is on the master slide.
119
- # @see Presentation#master?
120
- def master?
121
- presentation.master?(self)
122
- end
123
-
124
- # @return [Boolean] `true` if this asset is a Slide.
125
- def slide?
126
- false
127
- end
128
-
129
- # @param slide_name_or_index [Integer,String] the slide number of name to check for presence on.
130
- # @return [Boolean] `true` if this asset is present on the specified slide.
131
- # @see Presentation#has_slide?
132
- def has_slide?(slide_name_or_index)
133
- presentation.has_slide?(self,slide_name_or_index)
134
- end
135
-
136
- # @return [SlideCollection] an array-like collection of all slides that the asset is available on.
137
- # @see Presentation#slides_for
138
- def slides
139
- presentation.slides_for(self)
140
- end
141
-
142
- # @example
143
- # logo = app/"main:Scene.UI.Logo"
144
- # assert logo.master? # It's a master object
145
- #
146
- # show logo['endtime'].values #=> [10000,500,1000,750]
147
- # show logo['opacity'].values #=> [100,0,0,100]
148
- # logo1 = logo.on_slide(1)
149
- # logo2 = logo.on_slide(2)
150
- # show logo1['endtime'] #=> 500
151
- # show logo2['endtime'] #=> 1000
152
- # show logo1['opacity'] #=> 0
153
- # show logo2['opacity'] #=> 0
154
- #
155
- # logo2['opacity'] = 66
156
- # show logo['opacity'].values #=> [100,0,66,100]
157
- #
158
- # @param slide_name_or_index [Integer,String] the slide number or name to create the proxy for.
159
- # @return [SlideValues] a proxy that yields attribute values for a specific slide.
160
- def on_slide(slide_name_or_index)
161
- if has_slide?(slide_name_or_index)
162
- UIC::SlideValues.new( self, slide_name_or_index )
163
- end
164
- end
165
-
166
- # @return [String] the script path to this asset.
167
- # @see #path_to
168
- # @see Presentation#path_to
169
- def path
170
- @path ||= @presentation.path_to(self)
171
- end
172
-
173
- # @param other_asset [AssetBase] the asset to find the relative path to.
174
- # @return [String] the script path to another asset, relative to this one.
175
- # @see #path
176
- # @see Presentation#path_to
177
- def path_to(other_asset)
178
- @presentation.path_to(other_asset,self)
179
- end
180
-
181
- # @return [String] the name of this asset in the scene graph.
182
- def name
183
- properties['name'] ? properties['name'].get( self, presentation.slide_index(self) ) : type
184
- end
185
-
186
- # Change the name of the asset in the scene graph.
187
- # @param new_name [String] the new name for this asset.
188
- # @return [String] the new name.
189
- def name=( new_name )
190
- @path = nil # invalidate the memoization
191
- properties['name'].set( self, new_name, presentation.slide_index(self) )
192
- end
193
-
194
- # Get the value(s) of an attribute.
195
- # If `slide_name_or_index` is omitted, creates a ValuesPerSlide proxy for the specified attribute.
196
- # @example
197
- # logo = app/"main:Scene.UI.Logo"
198
- # show logo.master? #=> true (it's a master object)
199
- # show logo['endtime'].linked? #=> false (the endtime property is unlinked)
200
- # show logo['endtime'].values #=> [10000,500,1000,750]
201
- # show logo['endtime',0] #=> 10000 (the master slide value)
202
- # show logo['endtime',"Slide 1"] #=> 500
203
- # show logo['endtime',2] #=> 1000
204
- #
205
- # @param attribute_name [String,Symbol] the name of the attribute.
206
- # @param slide_name_or_index [Integer,String] the slide number or name to find the value on.
207
- # @return [Object] the value of the property on the given slide.
208
- # @return [ValuesPerSlide] if no slide is specified.
209
- # @see #on_slide
210
- # @see Presentation#get_attribute
211
- def [](attribute_name, slide_name_or_index=nil)
212
- if property = properties[attribute_name.to_s]
213
- if slide_name_or_index
214
- property.get( self, slide_name_or_index ) if has_slide?(slide_name_or_index)
215
- else
216
- UIC::ValuesPerSlide.new(@presentation,self,property)
217
- end
218
- end
219
- end
220
-
221
- # Set the value of an attribute, either across all slides, or on a particular slide.
222
- #
223
- # @example
224
- # logo = app/"main:Scene.UI.Logo"
225
- # show logo.master? #=> true (it's a master object)
226
- # show logo['endtime'].linked? #=> false (the endtime property is unlinked)
227
- # show logo['endtime'].values #=> [10000,500,1000,750]
228
- #
229
- # logo['endtime',1] = 99
230
- # show logo['endtime'].values #=> [10000,99,1000,750]
231
- #
232
- # logo['endtime'] = 42
233
- # show logo['endtime'].values #=> [42,42,42,42]
234
- # show logo['endtime'].linked? #=> false (the endtime property is still unlinked)
235
- #
236
- # @param attribute_name [String,Symbol] the name of the attribute.
237
- # @param slide_name_or_index [Integer,String] the slide number or name to set the value on.
238
- # @param new_value [Numeric,String] the new value for the attribute.
239
- # @see Presentation#set_attribute
240
- def []=( attribute_name, slide_name_or_index=nil, new_value )
241
- if property = properties[attribute_name.to_s] then
242
- property.set(self,new_value,slide_name_or_index)
243
- end
244
- end
245
-
246
- # @return [String] the XML representation of the scene graph element.
247
- def to_xml
248
- @el.to_xml
249
- end
250
-
251
- # @private no need to document this
252
- def to_s
253
- "<#{type} #{path}>"
254
- end
255
- alias_method :inspect, :to_s
256
-
257
- # @private no need to document this
258
- def ==(other)
259
- (self.class==other.class) && (el==other.el)
260
- end
261
- alias_method :eql?, :==
262
- end
263
-
264
- attr_reader :by_name
265
-
266
- HIER = {}
267
-
268
- %w[Asset Slide Scene].each{ |s| HIER[s] = 'AssetBase' }
269
- %w[Node Behavior Effect Image Layer MaterialBase PathAnchorPoint RenderPlugin].each{ |s| HIER[s]='Asset' }
270
- %w[Alias Camera Component Group Light Model Text Path].each{ |s| HIER[s]='Node' }
271
- %w[Material ReferencedMaterial].each{ |s| HIER[s]='MaterialBase' }
272
-
273
- def initialize(xml)
274
- @by_name = {'AssetBase'=>AssetBase}
275
-
276
- doc = Nokogiri.XML(xml)
277
- hack_in_slide_names!(doc)
278
-
279
- HIER.each do |class_name,parent_class_name|
280
- parent_class = @by_name[parent_class_name]
281
- el = doc.root.at(class_name)
282
- @by_name[class_name] = create_class(el,parent_class,el.name)
283
- end
284
-
285
- # Extend well-known classes with script interfaces after they are created
286
- @by_name['State'] = @by_name['Slide']
287
- @by_name['Slide'].instance_eval do
288
- attr_accessor :index, :name
289
- define_method :inspect do
290
- "<slide ##{index} of #{@el['component'] || @el.parent['component']}>"
291
- end
292
- define_method(:slide?){ true }
293
- end
294
-
295
- refmat = @by_name['ReferencedMaterial']
296
- @by_name['MaterialBase'].instance_eval do
297
- define_method :replace_with_referenced_material do
298
- type=='ReferencedMaterial' ? self : presentation.replace_asset( self, 'ReferencedMaterial', name:name )
299
- end
300
- end
301
-
302
- @by_name['Path'].instance_eval do
303
- define_method(:anchors){ find _type:'PathAnchorPoint' }
304
- end
305
-
306
- end
307
-
308
- # Creates a class from MetaData.xml with accessors for the <Property> listed.
309
- # Instances of the class are associated with a presentation and know how to
310
- # get/set values in that XML based on value types, slides, defaults.
311
- # Also used to create classes from effects, materials, and behavior preambles.
312
- # @param el [Nokogiri::XML::Element] the element in MetaData.xml representing this class.
313
- # @param parent_class [Class] the asset class to inherit from.
314
- # @param name [String] the name of this class.
315
- # @param new_defaults [Hash] hash mapping attribute name to a custom default value (as string) for this class.
316
- def create_class(el,parent_class,name,new_defaults={})
317
- Class.new(parent_class) do
318
- @name = name.to_s
319
- @properties = Hash[ el.css("Property").map do |e|
320
- type = e['type'] || (e['list'] ? 'String' : 'Float')
321
- type = "Float" if type=="float"
322
- property = UIC::Property.const_get(type).new(e)
323
- new_defaults.delete(property.name)
324
- [ property.name, property ]
325
- end ]
326
-
327
- new_defaults.each do |name,value|
328
- if prop=properties[name] # look in ancestor classes
329
- @properties[name] = prop.dup
330
- @properties[name].default = value
331
- end
332
- end
333
-
334
- def self.inspect
335
- @name
336
- end
337
- end
338
- end
339
-
340
- def new_instance(presentation,el)
341
- klass = @by_name[el.name] || create_class(el,@by_name['Asset'],el.name)
342
- klass.new(presentation,el)
343
- end
344
-
345
- def hack_in_slide_names!(doc)
346
- doc.at('Slide') << '<Property name="name" formalName="Name" type="String" default="Slide" hidden="True" />'
347
- end
348
- end
349
-
350
- def UIC.MetaData(metadata_path)
351
- raise %Q{Cannot find MetaData.xml at "#{metadata_path}"} unless File.exist?(metadata_path)
352
- UIC::MetaData.new(File.read(metadata_path,encoding:'utf-8'))
353
- end
354
-
355
- class UIC::SlideCollection
356
- include Enumerable
357
- attr_reader :length
358
- def initialize(slides)
359
- @length = slides.length-1
360
- @slides = slides
361
- @lookup = {}
362
- slides.each do |s|
363
- @lookup[s.index] = s
364
- @lookup[s.name] = s
365
- end
366
- end
367
- def each
368
- @slides.each{ |s| yield(s) }
369
- end
370
- def [](index_or_name)
371
- @lookup[ index_or_name ]
372
- end
373
- def inspect
374
- "[ #{@slides.map(&:inspect).join ', '} ]"
375
- end
376
- def to_ary
377
- @slides
378
- end
379
- end
380
-
381
- class UIC::ValuesPerSlide
382
- def initialize(presentation,asset,property)
383
- raise unless presentation.is_a?(UIC::Presentation)
384
-
385
- raise unless asset.is_a?(UIC::MetaData::AssetBase)
386
- raise unless property.is_a?(UIC::Property)
387
- @preso = presentation
388
- @asset = asset
389
- @el = asset.el
390
- @property = property
391
- end
392
- def value
393
- values.first
394
- end
395
- def [](slide_name_or_index)
396
- @property.get( @asset, slide_name_or_index )
397
- end
398
- def []=(slide_name_or_index,new_value)
399
- @property.set( @asset, new_value, slide_name_or_index )
400
- end
401
- def linked?
402
- @preso.attribute_linked?( @asset, @property.name )
403
- end
404
- def unlink
405
- @preso.unlink_attribute( @asset, @property.name )
406
- end
407
- def link
408
- @preso.link_attribute( @asset, @property.name )
409
- end
410
- def values
411
- @asset.slides.map{ |s| self[s.name] }
412
- end
413
- def inspect
414
- "<Values of '#{@asset.name}.#{@property.name}' across slides>"
415
- end
416
- alias_method :to_s, :inspect
417
- end
418
-
419
- class UIC::SlideValues
420
- attr_reader :asset
421
- def initialize( asset, slide )
422
- @asset = asset
423
- @slide = slide
424
- end
425
- def [](attribute_name)
426
- @asset[attribute_name,@slide]
427
- end
428
- def []=( attribute_name, new_value )
429
- @asset[attribute_name,@slide] = new_value
430
- end
431
- def method_missing( name, *args, &blk )
432
- asset.send(name,*args,&blk)
433
- end
434
- def inspect
435
- "<#{@asset.inspect} on slide #{@slide.inspect}>"
436
- end
1
+ #encoding: utf-8
2
+ class UIC::MetaData
3
+
4
+ # The base class for all assets. All other classes are dynamically created when a `MetaData.xml` file is loaded.
5
+ class AssetBase
6
+ @properties = {}
7
+ @name = "AssetBase"
8
+
9
+ class << self
10
+ # @return [String] The scene graph name of the asset.
11
+ attr_reader :name
12
+
13
+ # @return [Hash] a hash mapping attribute names to {Property} instances.
14
+ def properties
15
+ (ancestors[1].respond_to?(:properties) ? ancestors[1].properties : {}).merge(@properties)
16
+ end
17
+
18
+ # @private
19
+ def inspect
20
+ "<#{@name}>"
21
+ end
22
+ end
23
+
24
+ # @return [Hash] a hash mapping attribute names to {Property} instances.
25
+ def properties
26
+ self.class.properties
27
+ end
28
+
29
+ # Find an asset by relative scripting path.
30
+ #
31
+ # @example
32
+ # preso = app.main
33
+ # layer = preso/"Scene.Layer"
34
+ # cam1 = app/"main:Scene.Layer.Camera"
35
+ # cam2 = preso/"Scene.Layer.Camera"
36
+ # cam3 = layer/"Camera"
37
+ # cam4 = cam1/"parent.Camera"
38
+ #
39
+ # assert cam1==cam2 && cam2==cam3 && cam3==cam4
40
+ #
41
+ # @return [MetaData::AssetBase] The found asset, or `nil` if it cannot be found.
42
+ #
43
+ # @see Application#at
44
+ # @see Presentation#at
45
+ def at(sub_path)
46
+ presentation.at(sub_path,@el)
47
+ end
48
+ alias_method :/, :at
49
+
50
+ # @return [Presentation] the presentation that this asset is part of.
51
+ attr_accessor :presentation
52
+
53
+ # @return [Nokogiri::XML::Element] the internal XML element in the scene graph of the presentation.
54
+ attr_accessor :el
55
+
56
+ # Create a new asset. This is called for you automatically; you likely should not be using it.
57
+ # @param presentation [Presentation] the presentation owning this asset.
58
+ # @param element [Nokogiri::XML::Element] the internal XML element in the scene graph of the presentation.
59
+ def initialize( presentation, element )
60
+ @presentation = presentation
61
+ @el = element
62
+ end
63
+
64
+ # @return [String] the type of this asset. For example: `"Model"`, `"Material"`, `"ReferencedMaterial"`, `"PathAnchorPoint"`, etc.
65
+ def type
66
+ self.class.name
67
+ end
68
+
69
+ # @return [AssetBase] the parent of this asset in the scene graph.
70
+ # @see Presentation#parent_asset
71
+ def parent
72
+ presentation.parent_asset(self)
73
+ end
74
+
75
+ # @return [Array<AssetBase>] array of child assets in the scene graph. Children are in scene graph order.
76
+ # @see Presentation#child_assets
77
+ def children
78
+ presentation.child_assets(self)
79
+ end
80
+
81
+ # @return [String] the hierarchy under the element (for debugging purposes).
82
+ def hierarchy
83
+ presentation.hierarchy(self)
84
+ end
85
+
86
+ # Find descendant assets matching criteria.
87
+ # This method is the same as (but more convenient than) {Presentation#find} using the `:_under` criteria.
88
+ # See that method for documentation of the `criteria`.
89
+ #
90
+ # @example
91
+ # preso = app.main
92
+ # group = preso/"Scene.Layer.Vehicle"
93
+ # tires = group.find name:/^Tire/
94
+ #
95
+ # # alternative
96
+ # tires = preso.find name:/^Tire/, _under:group
97
+ #
98
+ # @return [Array<AssetBase>]
99
+ #
100
+ # @see Presentation#find
101
+ def find(criteria={},&block)
102
+ criteria[:_under] ||= self
103
+ presentation.find(criteria,&block)
104
+ end
105
+
106
+ # @return [AssetBase] the component or scene that owns this asset.
107
+ # If this asset is a component, does not return itself.
108
+ # @see Presentation#owning_component
109
+ def component
110
+ presentation.owning_component(self)
111
+ end
112
+
113
+ # @return [Boolean] `true` if this asset is a component or Scene.
114
+ def component?
115
+ @el.name=='Component' || @el.name=='Scene'
116
+ end
117
+
118
+ # @return [Boolean] `true` if this asset is on the master slide.
119
+ # @see Presentation#master?
120
+ def master?
121
+ presentation.master?(self)
122
+ end
123
+
124
+ # @return [Boolean] `true` if this asset is a Slide.
125
+ def slide?
126
+ false
127
+ end
128
+
129
+ # @param slide_name_or_index [Integer,String] the slide number of name to check for presence on.
130
+ # @return [Boolean] `true` if this asset is present on the specified slide.
131
+ # @see Presentation#has_slide?
132
+ def has_slide?(slide_name_or_index)
133
+ presentation.has_slide?(self,slide_name_or_index)
134
+ end
135
+
136
+ # @return [SlideCollection] an array-like collection of all slides that the asset is available on.
137
+ # @see Presentation#slides_for
138
+ def slides
139
+ presentation.slides_for(self)
140
+ end
141
+
142
+ # @example
143
+ # logo = app/"main:Scene.UI.Logo"
144
+ # assert logo.master? # It's a master object
145
+ #
146
+ # show logo['endtime'].values #=> [10000,500,1000,750]
147
+ # show logo['opacity'].values #=> [100,0,0,100]
148
+ # logo1 = logo.on_slide(1)
149
+ # logo2 = logo.on_slide(2)
150
+ # show logo1['endtime'] #=> 500
151
+ # show logo2['endtime'] #=> 1000
152
+ # show logo1['opacity'] #=> 0
153
+ # show logo2['opacity'] #=> 0
154
+ #
155
+ # logo2['opacity'] = 66
156
+ # show logo['opacity'].values #=> [100,0,66,100]
157
+ #
158
+ # @param slide_name_or_index [Integer,String] the slide number or name to create the proxy for.
159
+ # @return [SlideValues] a proxy that yields attribute values for a specific slide.
160
+ def on_slide(slide_name_or_index)
161
+ if has_slide?(slide_name_or_index)
162
+ UIC::SlideValues.new( self, slide_name_or_index )
163
+ end
164
+ end
165
+
166
+ # @return [String] the script path to this asset.
167
+ # @see #path_to
168
+ # @see Presentation#path_to
169
+ def path
170
+ @path ||= @presentation.path_to(self)
171
+ end
172
+
173
+ # @param other_asset [AssetBase] the asset to find the relative path to.
174
+ # @return [String] the script path to another asset, relative to this one.
175
+ # @see #path
176
+ # @see Presentation#path_to
177
+ def path_to(other_asset)
178
+ @presentation.path_to(other_asset,self)
179
+ end
180
+
181
+ # @return [String] the name of this asset in the scene graph.
182
+ def name
183
+ properties['name'] ? properties['name'].get( self, presentation.slide_index(self) ) : type
184
+ end
185
+
186
+ # Change the name of the asset in the scene graph.
187
+ # @param new_name [String] the new name for this asset.
188
+ # @return [String] the new name.
189
+ def name=( new_name )
190
+ @path = nil # invalidate the memoization
191
+ properties['name'].set( self, new_name, presentation.slide_index(self) )
192
+ end
193
+
194
+ # Get the value(s) of an attribute.
195
+ # If `slide_name_or_index` is omitted, creates a ValuesPerSlide proxy for the specified attribute.
196
+ # @example
197
+ # logo = app/"main:Scene.UI.Logo"
198
+ # show logo.master? #=> true (it's a master object)
199
+ # show logo['endtime'].linked? #=> false (the endtime property is unlinked)
200
+ # show logo['endtime'].values #=> [10000,500,1000,750]
201
+ # show logo['endtime',0] #=> 10000 (the master slide value)
202
+ # show logo['endtime',"Slide 1"] #=> 500
203
+ # show logo['endtime',2] #=> 1000
204
+ #
205
+ # @param attribute_name [String,Symbol] the name of the attribute.
206
+ # @param slide_name_or_index [Integer,String] the slide number or name to find the value on.
207
+ # @return [Object] the value of the property on the given slide.
208
+ # @return [ValuesPerSlide] if no slide is specified.
209
+ # @see #on_slide
210
+ # @see Presentation#get_attribute
211
+ def [](attribute_name, slide_name_or_index=nil)
212
+ if property = properties[attribute_name.to_s]
213
+ if slide_name_or_index
214
+ property.get( self, slide_name_or_index ) if has_slide?(slide_name_or_index)
215
+ else
216
+ UIC::ValuesPerSlide.new(@presentation,self,property)
217
+ end
218
+ end
219
+ end
220
+
221
+ # Set the value of an attribute, either across all slides, or on a particular slide.
222
+ #
223
+ # @example
224
+ # logo = app/"main:Scene.UI.Logo"
225
+ # show logo.master? #=> true (it's a master object)
226
+ # show logo['endtime'].linked? #=> false (the endtime property is unlinked)
227
+ # show logo['endtime'].values #=> [10000,500,1000,750]
228
+ #
229
+ # logo['endtime',1] = 99
230
+ # show logo['endtime'].values #=> [10000,99,1000,750]
231
+ #
232
+ # logo['endtime'] = 42
233
+ # show logo['endtime'].values #=> [42,42,42,42]
234
+ # show logo['endtime'].linked? #=> false (the endtime property is still unlinked)
235
+ #
236
+ # @param attribute_name [String,Symbol] the name of the attribute.
237
+ # @param slide_name_or_index [Integer,String] the slide number or name to set the value on.
238
+ # @param new_value [Numeric,String] the new value for the attribute.
239
+ # @see Presentation#set_attribute
240
+ def []=( attribute_name, slide_name_or_index=nil, new_value )
241
+ if property = properties[attribute_name.to_s] then
242
+ property.set(self,new_value,slide_name_or_index)
243
+ end
244
+ end
245
+
246
+ # @return [String] the XML representation of the scene graph element.
247
+ def to_xml
248
+ @el.to_xml
249
+ end
250
+
251
+ # @private no need to document this
252
+ def to_s
253
+ "<#{type} #{path}>"
254
+ end
255
+ alias_method :inspect, :to_s
256
+
257
+ # @private no need to document this
258
+ def ==(other)
259
+ (self.class==other.class) && (el==other.el)
260
+ end
261
+ alias_method :eql?, :==
262
+ end
263
+
264
+ attr_reader :by_name
265
+
266
+ HIER = {}
267
+
268
+ %w[Asset Slide Scene].each{ |s| HIER[s] = 'AssetBase' }
269
+ %w[Node Behavior Effect Image Layer MaterialBase PathAnchorPoint RenderPlugin].each{ |s| HIER[s]='Asset' }
270
+ %w[Alias Camera Component Group Light Model Text Path].each{ |s| HIER[s]='Node' }
271
+ %w[Material ReferencedMaterial].each{ |s| HIER[s]='MaterialBase' }
272
+
273
+ def initialize(xml)
274
+ @by_name = {'AssetBase'=>AssetBase}
275
+
276
+ doc = Nokogiri.XML(xml)
277
+ hack_in_slide_names!(doc)
278
+
279
+ HIER.each do |class_name,parent_class_name|
280
+ parent_class = @by_name[parent_class_name]
281
+ el = doc.root.at(class_name)
282
+ @by_name[class_name] = create_class(el,parent_class,el.name)
283
+ end
284
+
285
+ # Extend well-known classes with script interfaces after they are created
286
+ @by_name['State'] = @by_name['Slide']
287
+ @by_name['Slide'].instance_eval do
288
+ attr_accessor :index, :name
289
+ define_method :inspect do
290
+ "<slide ##{index} of #{@el['component'] || @el.parent['component']}>"
291
+ end
292
+ define_method(:slide?){ true }
293
+ end
294
+
295
+ refmat = @by_name['ReferencedMaterial']
296
+ @by_name['MaterialBase'].instance_eval do
297
+ define_method :replace_with_referenced_material do
298
+ type=='ReferencedMaterial' ? self : presentation.replace_asset( self, 'ReferencedMaterial', name:name )
299
+ end
300
+ end
301
+
302
+ @by_name['Path'].instance_eval do
303
+ define_method(:anchors){ find _type:'PathAnchorPoint' }
304
+ end
305
+
306
+ end
307
+
308
+ # Creates a class from MetaData.xml with accessors for the <Property> listed.
309
+ # Instances of the class are associated with a presentation and know how to
310
+ # get/set values in that XML based on value types, slides, defaults.
311
+ # Also used to create classes from effects, materials, and behavior preambles.
312
+ # @param el [Nokogiri::XML::Element] the element in MetaData.xml representing this class.
313
+ # @param parent_class [Class] the asset class to inherit from.
314
+ # @param name [String] the name of this class.
315
+ # @param new_defaults [Hash] hash mapping attribute name to a custom default value (as string) for this class.
316
+ def create_class(el,parent_class,name,new_defaults={})
317
+ Class.new(parent_class) do
318
+ @name = name.to_s
319
+ @properties = Hash[ el.css("Property").map do |e|
320
+ type = e['type'] || (e['list'] ? 'String' : 'Float')
321
+ type = "Float" if type=="float"
322
+ property = begin
323
+ UIC::Property.const_get(type).new(e)
324
+ rescue NameError
325
+ warn "WARNING: Unsupported property type '#{type}' on\n#{e}\nTreating this as a String."
326
+ UIC::Property::String.new(e)
327
+ end
328
+ new_defaults.delete(property.name)
329
+ [ property.name, property ]
330
+ end ]
331
+
332
+ new_defaults.each do |name,value|
333
+ if prop=properties[name] # look in ancestor classes
334
+ @properties[name] = prop.dup
335
+ @properties[name].default = value
336
+ end
337
+ end
338
+
339
+ def self.inspect
340
+ @name
341
+ end
342
+ end
343
+ end
344
+
345
+ def new_instance(presentation,el)
346
+ klass = @by_name[el.name] || create_class(el,@by_name['Asset'],el.name)
347
+ klass.new(presentation,el)
348
+ end
349
+
350
+ def hack_in_slide_names!(doc)
351
+ doc.at('Slide') << '<Property name="name" formalName="Name" type="String" default="Slide" hidden="True" />'
352
+ end
353
+ end
354
+
355
+ def UIC.MetaData(metadata_path)
356
+ raise %Q{Cannot find MetaData.xml at "#{metadata_path}"} unless File.exist?(metadata_path)
357
+ UIC::MetaData.new(File.read(metadata_path,encoding:'utf-8'))
358
+ end
359
+
360
+ class UIC::SlideCollection
361
+ include Enumerable
362
+ attr_reader :length
363
+ def initialize(slides)
364
+ @length = slides.length-1
365
+ @slides = slides
366
+ @lookup = {}
367
+ slides.each do |s|
368
+ @lookup[s.index] = s
369
+ @lookup[s.name] = s
370
+ end
371
+ end
372
+ def each
373
+ @slides.each{ |s| yield(s) }
374
+ end
375
+ def [](index_or_name)
376
+ @lookup[ index_or_name ]
377
+ end
378
+ def inspect
379
+ "[ #{@slides.map(&:inspect).join ', '} ]"
380
+ end
381
+ def to_ary
382
+ @slides
383
+ end
384
+ end
385
+
386
+ class UIC::ValuesPerSlide
387
+ def initialize(presentation,asset,property)
388
+ raise unless presentation.is_a?(UIC::Presentation)
389
+
390
+ raise unless asset.is_a?(UIC::MetaData::AssetBase)
391
+ raise unless property.is_a?(UIC::Property)
392
+ @preso = presentation
393
+ @asset = asset
394
+ @el = asset.el
395
+ @property = property
396
+ end
397
+ def value
398
+ values.first
399
+ end
400
+ def [](slide_name_or_index)
401
+ @property.get( @asset, slide_name_or_index )
402
+ end
403
+ def []=(slide_name_or_index,new_value)
404
+ @property.set( @asset, new_value, slide_name_or_index )
405
+ end
406
+ def linked?
407
+ @preso.attribute_linked?( @asset, @property.name )
408
+ end
409
+ def unlink
410
+ @preso.unlink_attribute( @asset, @property.name )
411
+ end
412
+ def link
413
+ @preso.link_attribute( @asset, @property.name )
414
+ end
415
+ def values
416
+ @asset.slides.map{ |s| self[s.name] }
417
+ end
418
+ def inspect
419
+ "<Values of '#{@asset.name}.#{@property.name}' across slides>"
420
+ end
421
+ alias_method :to_s, :inspect
422
+ end
423
+
424
+ class UIC::SlideValues
425
+ attr_reader :asset
426
+ def initialize( asset, slide )
427
+ @asset = asset
428
+ @slide = slide
429
+ end
430
+ def [](attribute_name)
431
+ @asset[attribute_name,@slide]
432
+ end
433
+ def []=( attribute_name, new_value )
434
+ @asset[attribute_name,@slide] = new_value
435
+ end
436
+ def method_missing( name, *args, &blk )
437
+ asset.send(name,*args,&blk)
438
+ end
439
+ def inspect
440
+ "<#{@asset.inspect} on slide #{@slide.inspect}>"
441
+ end
437
442
  end