flame_channel_parser 2.0.1 → 2.1.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.
data/History.txt CHANGED
@@ -1,3 +1,12 @@
1
+ === 2.1.1 / 2011-23-06
2
+
3
+ * Fixed exceptions for for channel not found errors
4
+ * Uses cli_test for testing the application
5
+
6
+ === 2.1.0 / 2011-23-06
7
+
8
+ * Fixed range detection for setups (we now properly detect the last frame for setups that support it)
9
+
1
10
  === 2.0.1 / 2011-23-06
2
11
 
3
12
  * Fix the bake_flame_channel binary to use the proper extractor API
data/Rakefile CHANGED
@@ -3,14 +3,15 @@
3
3
  require 'rubygems'
4
4
  require 'hoe'
5
5
 
6
+ Hoe::RUBY_FLAGS.gsub!(/^\-w/, '') # No thanks undefined ivar warnings
6
7
  Hoe.spec 'flame_channel_parser' do | p |
7
- Hoe::RUBY_FLAGS.gsub!(/^\-w/, '') # No thanks undefined ivar warnings
8
-
8
+ p.developer('Julik Tarkhanov', 'me@julik.nl')
9
+
9
10
  p.readme_file = 'README.rdoc'
10
11
  p.extra_rdoc_files = FileList['*.rdoc'] + FileList['*.txt']
11
12
  p.extra_deps = {"update_hints" => ">=0" }
12
- p.developer('Julik Tarkhanov', 'me@julik.nl')
13
- p.clean_globs = %w( **/.DS_Store coverage.info **/*.rbc .idea .yardoc *.numbers)
13
+ p.extra_dev_deps = {"cli_test" => "~> 1.0.0" }
14
+ p.clean_globs = File.read(".gitignore").split("\n")
14
15
  end
15
16
 
16
17
  # vim: syntax=ruby
@@ -30,6 +30,10 @@ op.on(" -s", "--startframe FRAME", Integer,
30
30
  op.on(" -e", "--endframe FRAME", Integer,
31
31
  "Bake the curve upto this specific frame (defaults to the last keyframe in the setup)"
32
32
  ) {|upto| options[:end_frame] = upto }
33
+ op.on(" -k", "--keyframed-range-only",
34
+ "Bake the curve from the first keyframe to the last only (overrides --startframe and --endframe)"
35
+ ) { options[:on_curve_limits] = true }
36
+
33
37
  op.on(" -f", "--to-file FILENAME", String,
34
38
  "Write the curve to a file at this path instead of printing it to STDOUT"
35
39
  ) {|path| options[:destination] = File.open(path, "wb") }
data/lib/extractor.rb CHANGED
@@ -2,7 +2,6 @@
2
2
  class FlameChannelParser::Extractor
3
3
 
4
4
  DEFAULT_CHANNEL_TO_EXTRACT = "Timing/Timing"
5
- DEFAULTS = {:destination => $stdout, :start_frame => nil, :end_frame => nil, :channel => DEFAULT_CHANNEL_TO_EXTRACT }
6
5
 
7
6
  # Raised when a channel is not found in the setup file
8
7
  class ChannelNotFoundError < RuntimeError; end
@@ -10,9 +9,6 @@ class FlameChannelParser::Extractor
10
9
  # Raised when you try to autodetect the length of a channel that has no keyframes
11
10
  class NoKeyframesError < RuntimeError; end
12
11
 
13
- # Raised when you try to bake 0 or negative amount of frames
14
- class EmptySegmentError < RuntimeError; end
15
-
16
12
  # Pass the path to Flame setup here and you will get the animation curve on the object passed in
17
13
  # the :destionation option (defaults to STDOUT). The following options are accepted:
18
14
  #
@@ -27,43 +23,91 @@ class FlameChannelParser::Extractor
27
23
  # 1 123.456
28
24
  # 2 124.567
29
25
  def self.extract(path, options = {})
30
- options = DEFAULTS.merge(options)
26
+ new.extract(path, options)
27
+ end
28
+
29
+ def extract(path, options)
30
+ options = DEFAULTS.dup.merge(options)
31
31
  File.open(path) do |f|
32
+
33
+ # Then parse
32
34
  channels = FlameChannelParser.parse(f)
33
35
  selected_channel = find_channel_in(channels, options[:channel])
34
36
  interpolator = FlameChannelParser::Interpolator.new(selected_channel)
37
+
38
+ # Configure the range
39
+ configure_start_and_end_frame(f, options, interpolator)
40
+
41
+ # And finally...
35
42
  write_channel(interpolator, options[:destination], options[:start_frame], options[:end_frame])
36
43
  end
37
44
  end
38
45
 
39
46
  private
40
47
 
41
- def self.find_channel_in(channels, channel_path)
48
+ DEFAULT_START_FRAME = 1
49
+ DEFAULTS = {:destination => $stdout, :start_frame => nil, :end_frame => nil, :channel => DEFAULT_CHANNEL_TO_EXTRACT, :on_curve_limits => false }
50
+
51
+ SETUP_END_FRAME_PATTERN = /(MaxFrames|Frames)(\s+)(\d+)/
52
+ SETUP_START_FRAME_PATTERN = /(MinFrame)(\s+)(\d+)/
53
+
54
+ def configure_start_and_end_frame(f, options, interpolator)
55
+ # If the settings specify last and first frame...
56
+ if options[:on_curve_limits]
57
+ options[:start_frame] = interpolator.first_defined_frame.to_i
58
+ options[:end_frame] = interpolator.last_defined_frame.to_i
59
+ if (!options[:start_frame] || !options[:end_frame])
60
+ raise NoKeyframesError, "This channel probably has no animation so there " +
61
+ "is no way to automatically tell how many keyframes it has. " +
62
+ "Please set the start and end frame explicitly."
63
+ elsif options[:end_frame] == options[:start_frame]
64
+ raise NoKeyframesError, "This channel has only one keyframe " +
65
+ "at frame #{options[:start_frame]}and baking it makes no sense."
66
+ end
67
+ else # Detect from the setup itself (the default)
68
+ # First try to detect start and end frames from the known flags
69
+ f.rewind
70
+ detected_start, detected_end = detect_start_and_end_frame_in_io(f)
71
+
72
+ options[:start_frame] = options[:start_frame] || detected_start || DEFAULT_START_FRAME
73
+ options[:end_frame] ||= detected_end
74
+ end
75
+ end
76
+
77
+
78
+ def detect_start_and_end_frame_in_io(io)
79
+ cur_offset, s, e = io.pos, nil, nil
80
+ io.rewind
81
+ while line = io.gets
82
+ if (elements = line.scan(SETUP_START_FRAME_PATTERN)).any?
83
+ s = elements.flatten[-1].to_i
84
+ elsif (elements = line.scan(SETUP_END_FRAME_PATTERN)).any?
85
+ e = elements.flatten[-1].to_i
86
+ return [s, e]
87
+ end
88
+ end
89
+ end
90
+
91
+ def compose_channel_not_found_message(for_channel, other_channels)
92
+ message = "Channel #{for_channel.inspect} not found in this setup (set the channel with the :channel option). Found other channels though:"
93
+ message << "\n"
94
+ message += other_channels.map{|c| "\t%s\n" % c.path }.join
95
+ end
96
+
97
+ def find_channel_in(channels, channel_path)
42
98
  selected_channel = channels.find{|c| channel_path == c.path }
43
99
  unless selected_channel
44
- message = "Channel #{channel_path.inspect} not found in this setup (set the channel with the --channel option). Found other channels though:"
45
- message << "\n"
46
- message += channels.map{|c| "\t%s\n" % c.path }.join
47
- raise ChannelNotFoundError, message
100
+ raise ChannelNotFoundError, compose_channel_not_found_message(channel_path, channels)
48
101
  end
49
102
  selected_channel
50
103
  end
51
-
52
- def self.write_channel(interpolator, to_io, start_frame, end_frame)
53
-
54
- from_frame = start_frame || interpolator.first_defined_frame
55
- to_frame = end_frame || interpolator.last_defined_frame
104
+
105
+ def write_channel(interpolator, to_io, from_frame_i, to_frame_i)
56
106
 
57
- unless (from_frame && to_frame)
58
- raise NoKeyframesError, "This channel probably has no animation so there is no way to automatically tell how many keyframes it has. " +
59
- "Please set the start and end frame explicitly."
107
+ if (to_frame_i - from_frame_i) == 1
108
+ $stderr.puts "WARNING: You are extracting one animation frame. Check the length of your setup, or set the range manually"
60
109
  end
61
110
 
62
- raise EmptySegmentError, "The segment you are trying to bake is too small (it has nothing in it)" if to_frame - from_frame < 1
63
-
64
- from_frame_i = from_frame.to_f.floor
65
- to_frame_i = to_frame.to_f.ceil
66
-
67
111
  (from_frame_i..to_frame_i).each do | frame |
68
112
  line = "%d\t%.5f\n" % [frame, interpolator.sample_at(frame)]
69
113
  to_io << line
@@ -1,5 +1,5 @@
1
1
  module FlameChannelParser
2
- VERSION = '2.0.1'
2
+ VERSION = '2.1.1'
3
3
 
4
4
  # Parse a Flame setup into an array of Channel
5
5
  def self.parse(io)
@@ -1,3 +1,43 @@
1
+ 1 459.00000
2
+ 2 457.00000
3
+ 3 455.00000
4
+ 4 453.00000
5
+ 5 451.00000
6
+ 6 449.00000
7
+ 7 447.00000
8
+ 8 445.00000
9
+ 9 443.00000
10
+ 10 441.00000
11
+ 11 439.00000
12
+ 12 437.00000
13
+ 13 435.00000
14
+ 14 433.00000
15
+ 15 431.00000
16
+ 16 429.00000
17
+ 17 427.00000
18
+ 18 425.00000
19
+ 19 423.00000
20
+ 20 421.00000
21
+ 21 419.00000
22
+ 22 417.00000
23
+ 23 415.00000
24
+ 24 413.00000
25
+ 25 411.00000
26
+ 26 409.00000
27
+ 27 407.00000
28
+ 28 405.00000
29
+ 29 403.00000
30
+ 30 401.00000
31
+ 31 399.00000
32
+ 32 397.00000
33
+ 33 395.00000
34
+ 34 393.00000
35
+ 35 391.00000
36
+ 36 389.00000
37
+ 37 387.00000
38
+ 38 385.00000
39
+ 39 383.00000
40
+ 40 381.00000
1
41
  41 379.00000
2
42
  42 377.00000
3
43
  43 375.00000
data/test/test_cli.rb CHANGED
@@ -1,58 +1,57 @@
1
1
  require "test/unit"
2
2
  require "stringio"
3
3
  require "tempfile"
4
+ require "cli_test"
4
5
 
5
- require File.dirname(__FILE__) + "/../lib/flame_channel_parser"
6
-
7
- class CliTest < Test::Unit::TestCase
8
- BIN_P = File.expand_path(File.dirname(__FILE__) + "/../bin/bake_flame_channel")
6
+ class TestCli < Test::Unit::TestCase
9
7
 
10
- # Run the binary under test with passed options, and return [exit_code, stdout_content, stderr_content]
11
- def cli(commandline_arguments)
12
- old_stdout, old_stderr, old_argv = $stdout, $stderr, ARGV.dup
13
- os, es = StringIO.new, StringIO.new
14
- begin
15
- $stdout, $stderr, verbosity = os, es, $VERBOSE
16
- ARGV.replace(commandline_arguments.split)
17
- $VERBOSE = false
18
- load(BIN_P)
19
- return [0, os.string, es.string]
20
- rescue SystemExit => boom # The binary uses exit(), we use that to preserve the output code
21
- return [boom.status, os.string, es.string]
22
- ensure
23
- $VERBOSE = verbosity
24
- ARGV.replace(old_argv)
25
- $stdout, $stderr = old_stdout, old_stderr
26
- end
8
+ def setup
9
+ binary = File.expand_path(File.dirname(__FILE__) + "/../bin/bake_flame_channel")
10
+ @app = CLITest.new(binary)
27
11
  end
28
12
 
29
13
  def test_cli_with_no_args_produces_usage
30
- status, o, e = cli('')
14
+ status, o, e = @app.run('')
31
15
  assert_equal -1, status
32
16
  assert_match /No input file path provided/, e
33
17
  assert_match /--help for usage information/, e
34
18
  end
35
19
 
36
20
  def test_cli_with_nonexisting_file
37
- status, o, e = cli(" amazing.action")
21
+ status, o, e = @app.run(" amazing.action")
38
22
  assert_equal -1, status
39
23
  assert_match /does not exist/, e
40
24
  end
41
25
 
42
26
  def test_cli_with_proper_output
43
27
  full_path = File.expand_path(File.dirname(__FILE__)) + "/snaps/TW.timewarp"
44
- status, output, e = cli(" " + full_path)
28
+ status, output, e = @app.run(" " + full_path)
45
29
  assert_equal 0, status
46
- assert_equal 816, output.split("\n").length, "Should have output 816 frames"
30
+ assert_equal 747, output.split("\n").length, "Should have output 816 frames"
31
+ end
32
+
33
+ def test_cli_with_file_length
34
+ full_path = File.expand_path(File.dirname(__FILE__)) + "/snaps/TW_015_010_v03.timewarp"
35
+ status, output, e = @app.run(full_path)
36
+ assert_equal 0, status
37
+ assert_equal 476, output.split("\n").length, "Should have output 476 frames"
38
+ end
39
+
40
+ def test_cli_with_curve_limits
41
+ full_path = File.expand_path(File.dirname(__FILE__)) + "/snaps/TW_015_010_v03.timewarp"
42
+ status, output, e = @app.run(" --keyframed-range-only " + full_path)
43
+ assert_equal 0, status
44
+ assert_equal 531, output.split("\n").length, "Should have output 513 frames"
47
45
  end
48
46
 
49
47
  def test_cli_with_output_to_file
50
48
  tf = Tempfile.new("experiment")
51
49
  full_path = File.expand_path(File.dirname(__FILE__)) + "/snaps/TW.timewarp"
52
- status, output, e = cli(" --to-file " + tf.path + " " + full_path)
53
-
50
+ status, output, e = @app.run(" --to-file " + tf.path + " " + full_path)
51
+
54
52
  assert_equal 0, status
55
- assert_equal 816, File.read(tf.path).split("\n").length, "Should have output 816 frames"
53
+ assert_equal 0, output.length
54
+ assert_equal 747, File.read(tf.path).split("\n").length, "Should have output 816 frames"
56
55
  ensure
57
56
  tf.close!
58
57
  end
@@ -4,20 +4,14 @@ require "stringio"
4
4
  require File.dirname(__FILE__) + "/../lib/flame_channel_parser"
5
5
 
6
6
  class TestExtractor < Test::Unit::TestCase
7
+
7
8
  def test_basic_operation
8
9
  io = StringIO.new
9
- FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/RefT_Steadicam.timewarp", :destination => io)
10
+ opts = {:destination => io}
11
+ FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/RefT_Steadicam.timewarp", opts)
10
12
  assert_equal File.read(File.dirname(__FILE__) + "/snaps/RefT_Steadicam_Extraction.txt"), io.string
11
13
  end
12
14
 
13
- def test_channel_selection_by_path_raises_with_not_animated_channel_and_no_start_and_end
14
- io = StringIO.new
15
- ops = {:destination => io, :channel => "axis1/position/z"}
16
- assert_raise(FlameChannelParser::Extractor::NoKeyframesError) do
17
- FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/FLEM_curves_example_migrated_to_2012.action", ops)
18
- end
19
- end
20
-
21
15
  def test_channel_selection_by_path_outputs_properly
22
16
  io = StringIO.new
23
17
  ops = {:destination => io, :channel => "axis1/position/y"}
@@ -38,13 +32,32 @@ class TestExtractor < Test::Unit::TestCase
38
32
  io = StringIO.new
39
33
  o = {:destination => io, :start_frame => 19, :end_frame => 347 }
40
34
  FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/RefT_Steadicam.timewarp", o)
41
-
42
35
  assert_equal File.read(File.dirname(__FILE__) + "/snaps/RefT_Steadicam_Extraction_F19_to_347.txt"), io.string
43
36
  end
44
37
 
38
+ def test_properly_recognizes_timewarp_length_in_timewarp
39
+ io = StringIO.new
40
+ o = {:destination => io }
41
+ FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/TW_015_010_v03.timewarp", o)
42
+ lines = io.string.split("\n")
43
+
44
+ assert_equal 476, lines.length, "Should have parsed out 476 frames even though animation curves go further"
45
+ end
46
+
47
+ def test_properly_recognizes_timewarp_length_in_action
48
+ io = StringIO.new
49
+ o = {:destination => io , :channel => "axis1/position/z"}
50
+ FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/FLEM_BrokenTangents.action", o)
51
+ lines = io.string.split("\n")
52
+
53
+ assert_equal 125, lines.length, "Should have parsed out 125 frames even though animation curves go further"
54
+ end
55
+
45
56
  def test_constant_channels_need_domain_of_definition_on_time
57
+ opts = {:channel => "Mix/Mix", :on_curve_limits => true}
58
+
46
59
  assert_raise(FlameChannelParser::Extractor::NoKeyframesError) do
47
- FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/RefT_Steadicam.timewarp", :channel => "Mix/Mix")
60
+ FlameChannelParser::Extractor.extract(File.dirname(__FILE__) + "/snaps/RefT_Steadicam.timewarp", opts)
48
61
  end
49
62
  end
50
63
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: flame_channel_parser
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 2.0.1
5
+ version: 2.1.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Julik Tarkhanov
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-23 00:00:00 Z
13
+ date: 2011-06-24 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: update_hints
@@ -24,16 +24,27 @@ dependencies:
24
24
  type: :runtime
25
25
  version_requirements: *id001
26
26
  - !ruby/object:Gem::Dependency
27
- name: hoe
27
+ name: cli_test
28
28
  prerelease: false
29
29
  requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 1.0.0
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: hoe
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
30
41
  none: false
31
42
  requirements:
32
43
  - - ">="
33
44
  - !ruby/object:Gem::Version
34
45
  version: 2.9.4
35
46
  type: :development
36
- version_requirements: *id002
47
+ version_requirements: *id003
37
48
  description: |-
38
49
  Includes a small library for extracting, parsing and baking animation curves made on Discrodesk Floke/Inflinto, also known as flame.
39
50
  Thanks to Marijn Eken, Philippe Soeiro and Andre Gagnon for their support and advice.