kamelopard 0.0.11 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/kamelopard.rb +1 -0
- data/lib/kamelopard/classes.rb +14 -6
- data/lib/kamelopard/function.rb +30 -7
- data/lib/kamelopard/function_paths.rb +87 -23
- data/lib/kamelopard/helpers.rb +69 -7
- data/lib/kamelopard/multicam.rb +181 -0
- data/lib/kamelopard/spline.rb +83 -51
- metadata +3 -2
data/lib/kamelopard.rb
CHANGED
data/lib/kamelopard/classes.rb
CHANGED
@@ -108,6 +108,15 @@ module Kamelopard
|
|
108
108
|
def Kamelopard.convert_coord(a) # :nodoc:
|
109
109
|
a = a.to_s.upcase.strip.gsub(/\s+/, '')
|
110
110
|
|
111
|
+
if a =~ /^[+-]?\d+(\.\d+)?$/ then
|
112
|
+
# coord needs no transformation
|
113
|
+
return a.to_f
|
114
|
+
elsif a =~ /^[+-]?\d+\.\d+E?-?\d+/ then
|
115
|
+
# Scientific notation
|
116
|
+
return a.to_f
|
117
|
+
end
|
118
|
+
p a
|
119
|
+
|
111
120
|
mult = 1
|
112
121
|
if a =~ /^-/ then
|
113
122
|
mult *= -1
|
@@ -121,10 +130,7 @@ module Kamelopard
|
|
121
130
|
a = a.sub /[NESW]$/, ''
|
122
131
|
a = a.strip
|
123
132
|
|
124
|
-
if a =~ /^\d+(\.\d+)
|
125
|
-
# coord needs no transformation
|
126
|
-
1
|
127
|
-
elsif a =~ /^\d+D\d+M\d+(\.\d+)?S$/ then
|
133
|
+
if a =~ /^\d+D\d+M\d+(\.\d+)?S$/ then
|
128
134
|
# coord is in dms
|
129
135
|
p = a.split /[D"']/
|
130
136
|
a = p[0].to_f + (p[2].to_f / 60.0 + p[1].to_f) / 60.0
|
@@ -143,7 +149,7 @@ module Kamelopard
|
|
143
149
|
# check that it's within range
|
144
150
|
a = a.to_f * mult
|
145
151
|
raise "Coordinate #{a} out of range" if a > 180 or a < -180
|
146
|
-
return a
|
152
|
+
return a.to_f
|
147
153
|
end
|
148
154
|
|
149
155
|
# Helper function for altitudeMode / gx:altitudeMode elements
|
@@ -629,11 +635,13 @@ module Kamelopard
|
|
629
635
|
end
|
630
636
|
|
631
637
|
def roll
|
638
|
+
# The roll element doesn't exist in LookAt objects
|
632
639
|
raise "The roll element is part of Camera objects, not LookAt objects"
|
633
640
|
end
|
634
641
|
|
635
|
-
def roll=
|
642
|
+
def roll=(a)
|
636
643
|
# The roll element doesn't exist in LookAt objects
|
644
|
+
raise "The roll element is part of Camera objects, not LookAt objects"
|
637
645
|
end
|
638
646
|
end
|
639
647
|
|
data/lib/kamelopard/function.rb
CHANGED
@@ -14,7 +14,7 @@ module Kamelopard
|
|
14
14
|
module Functions
|
15
15
|
|
16
16
|
# Abstract class representing a one-dimensional function
|
17
|
-
class
|
17
|
+
class Function
|
18
18
|
# min and max describe the function's domain. Values passed to
|
19
19
|
# get_value will only range from 0 to 1; the actual value
|
20
20
|
# calculated will be mapped to a percentage of that domain.
|
@@ -42,7 +42,7 @@ module Kamelopard
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def compose=(f)
|
45
|
-
raise "Can only compose another
|
45
|
+
raise "Can only compose another function" unless f.kind_of? Function or f.nil?
|
46
46
|
@compose = f
|
47
47
|
end
|
48
48
|
|
@@ -57,7 +57,7 @@ module Kamelopard
|
|
57
57
|
end
|
58
58
|
|
59
59
|
#def append(f)
|
60
|
-
# raise "Can only append another one-dimensional function" unless f.kind_of?
|
60
|
+
# raise "Can only append another one-dimensional function" unless f.kind_of? Function or f.nil?
|
61
61
|
# print STDERR "WARNING: append() isn't actually implemented" unless f.nil?
|
62
62
|
# # XXX
|
63
63
|
# # Gotta implement this. The idea is to have one function for the first
|
@@ -74,9 +74,29 @@ module Kamelopard
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def self.interpolate(a, b)
|
77
|
-
# Creates a new
|
77
|
+
# Creates a new Function object between points A and B
|
78
78
|
raise "Override this method before calling it, please"
|
79
79
|
end
|
80
|
+
end ## End of Function class
|
81
|
+
|
82
|
+
# get_value and run_function return a single scalar value
|
83
|
+
class Function1D < Function
|
84
|
+
def compose=(f)
|
85
|
+
raise "Can only compose another one-dimensional function" unless f.kind_of? Function1D or f.nil?
|
86
|
+
@compose = f
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# get_value and run_function return an array of values
|
92
|
+
class FunctionMultiDim < Function
|
93
|
+
attr_reader :ndims
|
94
|
+
|
95
|
+
def compose=(f)
|
96
|
+
raise "Can only compose another #{@ndims}-dimensional function" unless (f.kind_of? FunctionMultiDim and @ndims = f.ndims) or f.nil?
|
97
|
+
@compose = f
|
98
|
+
end
|
99
|
+
|
80
100
|
end
|
81
101
|
|
82
102
|
# Represents a cubic equation of the form c3 * x^3 + c2 * x^2 + c1 * x + c0
|
@@ -105,7 +125,7 @@ module Kamelopard
|
|
105
125
|
c0 = m[0,3]
|
106
126
|
return Cubic.new(c3, c2, c1, c0, min, max)
|
107
127
|
end
|
108
|
-
end
|
128
|
+
end ## End of Cubic class
|
109
129
|
|
110
130
|
# Describes a quadratic equation
|
111
131
|
class Quadratic < Cubic
|
@@ -149,8 +169,11 @@ module Kamelopard
|
|
149
169
|
return Constant.new((b.to_f - a.to_f) / 0.0)
|
150
170
|
end
|
151
171
|
end
|
152
|
-
end
|
153
|
-
end
|
172
|
+
end ## End of Functions sub-module
|
173
|
+
end ## End of Kamelopard module
|
174
|
+
|
175
|
+
## Example uses
|
176
|
+
|
154
177
|
# include Kamelopard::Functions
|
155
178
|
#
|
156
179
|
# l = Line.new 1.0, 0.0
|
@@ -13,7 +13,7 @@ module Kamelopard
|
|
13
13
|
# points: The number of points in the series
|
14
14
|
# hash: Values used to create the hash, which creates the point in the
|
15
15
|
# series. Keys in this hash include:
|
16
|
-
#
|
16
|
+
# Any option suitable for the make_view_from() function
|
17
17
|
# These can be constant numbers, Proc objects, or Function1D objects.
|
18
18
|
# The latter two will be called once for each point in the series.
|
19
19
|
# Proc objects will be passed the number of the point they're
|
@@ -21,7 +21,7 @@ module Kamelopard
|
|
21
21
|
# created for this point. "duration" represents the time in seconds
|
22
22
|
# spent flying from the last point to this one.
|
23
23
|
# callback
|
24
|
-
# This Proc object, if defined, will be called after the
|
24
|
+
# This Proc object, if defined, will be called after the other hash
|
25
25
|
# keys have been calculated. It gets passed the number of the point,
|
26
26
|
# and the current value of the hash for this point. It can modify and
|
27
27
|
# return that hash as needed.
|
@@ -34,11 +34,33 @@ module Kamelopard
|
|
34
34
|
# If set, a placemark object will be created at this point
|
35
35
|
# no_flyto
|
36
36
|
# If set, on flyto objects will be created
|
37
|
-
|
37
|
+
# multidim
|
38
|
+
# An array of hashes. Each array element is an array, containing two
|
39
|
+
# values. The first is associated with a FunctionMultiDim class
|
40
|
+
# representing a multidimensional function. The second is an array of
|
41
|
+
# symbols and nils. Valid symbols include any of the possible
|
42
|
+
# make_function_path options, except :multidim. At execution, the
|
43
|
+
# FunctionMultiDim will be evaluated, returning an array of values.
|
44
|
+
# The symbols in the :vals array will be assigned the returned value
|
45
|
+
# corresponding to their position in the :vals array. For instance,
|
46
|
+
# assume the following :multidim argument
|
47
|
+
# [ { :func => myFunc, :vals = [:latitude, :longitude, nil, :altitude]} ]
|
48
|
+
# When myFunc is evaluated, assume it returns [1, 2, 3, 4, 5]. Thus,
|
49
|
+
# :latitude will be 1, :longitude 2, and so on. Because :vals[2] is
|
50
|
+
# nil, the corresponding element in the results of myFunc will be
|
51
|
+
# ignored. Also, given that :vals contains four values whereas myFunc
|
52
|
+
# returned 5, the unallocated final myFunc value will also be
|
53
|
+
# ignored.
|
54
|
+
# NOTE ON PROCESSING ORDER
|
55
|
+
# Individually specified hash options are processed first, followed by
|
56
|
+
# :multidim. So hash options included directly as
|
57
|
+
# well as in a :multidim :vals array will take the value from
|
58
|
+
# :multidim. make_function_path yields to code blocks last, after all
|
59
|
+
# other assignment.
|
38
60
|
def make_function_path(points = 10, options = {})
|
39
61
|
|
40
62
|
def val(a, b, c) # :nodoc:
|
41
|
-
if a.kind_of?
|
63
|
+
if a.kind_of? Function then
|
42
64
|
return a.get_value(c)
|
43
65
|
elsif a.kind_of? Proc then
|
44
66
|
return a.call(b, a)
|
@@ -47,50 +69,92 @@ module Kamelopard
|
|
47
69
|
end
|
48
70
|
end
|
49
71
|
|
50
|
-
|
72
|
+
views = []
|
73
|
+
placemarks = []
|
51
74
|
|
52
75
|
callback_value = nil
|
53
76
|
i = 0
|
54
77
|
while (i <= points)
|
55
78
|
p = i.to_f / points.to_f
|
56
|
-
hash = {
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
79
|
+
hash = {}
|
80
|
+
[ :latitude, :longitude, :altitude, :heading,
|
81
|
+
:tilt, :altitudeMode, :extrude, :when,
|
82
|
+
:roll, :range, :pause, :begin, :end, :show_placemarks,
|
83
|
+
:no_flyto, :pause
|
84
|
+
].each do |k|
|
85
|
+
if options.has_key? k then
|
86
|
+
hash[k] = val(options[k], i, p)
|
87
|
+
end
|
88
|
+
end
|
65
89
|
|
66
90
|
hash[:show_placemarks] = options[:show_placemarks] if options.has_key? :show_placemarks
|
67
|
-
hash[:roll] = val(options[:roll], i, p) if options.has_key? :roll
|
68
|
-
hash[:range] = val(options[:range], i, p) if options.has_key? :range
|
69
|
-
hash[:pause] = val(options[:pause], i, p) if options.has_key? :pause
|
91
|
+
#hash[:roll] = val(options[:roll], i, p) if options.has_key? :roll
|
92
|
+
#hash[:range] = val(options[:range], i, p) if options.has_key? :range
|
93
|
+
#hash[:pause] = val(options[:pause], i, p) if options.has_key? :pause
|
70
94
|
|
71
|
-
if
|
95
|
+
if options.has_key? :duration
|
72
96
|
duration = val(options[:duration], i, p)
|
73
97
|
else
|
74
98
|
duration = (i == 0 ? 0 : 2)
|
75
99
|
end
|
100
|
+
hash[:duration] = duration
|
101
|
+
|
102
|
+
if options.has_key? :multidim then
|
103
|
+
options[:multidim].each do |md|
|
104
|
+
r = val(md[0], i, p)
|
105
|
+
md[1].each_index do |ind|
|
106
|
+
hash[md[1][ind]] = r[0, ind] unless md[1][ind].nil?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
76
110
|
|
77
111
|
hash[:callback_value] = callback_value unless callback_value.nil?
|
78
|
-
|
79
|
-
|
112
|
+
|
113
|
+
begin
|
114
|
+
tmp = yield(i, hash)
|
115
|
+
hash = tmp unless tmp.nil?
|
116
|
+
rescue LocalJumpError
|
117
|
+
# Don't do anything; there's no block to yield to
|
118
|
+
end
|
80
119
|
#hash = options[:callback].call(i, hash) if options.has_key? :callback
|
81
120
|
callback_value = hash[:callback_value] if hash.has_key? :callback_value
|
82
121
|
|
83
122
|
v = make_view_from(hash)
|
84
123
|
p = point(v.longitude, v.latitude, v.altitude, hash[:altitudeMode], hash[:extrude])
|
85
|
-
|
124
|
+
# XXX Should I add the view's timestamp / timespan, if it exists, to the placemark?
|
125
|
+
pl = placemark(i.to_s, :geometry => p)
|
126
|
+
pl.abstractView = v
|
127
|
+
get_folder << pl if hash.has_key? :show_placemarks
|
86
128
|
fly_to v, :duration => duration , :mode => :smooth unless hash.has_key? :no_flyto
|
87
|
-
|
129
|
+
views << v
|
130
|
+
placemarks << pl
|
88
131
|
|
89
132
|
pause hash[:pause] if hash.has_key? :pause
|
90
133
|
|
91
134
|
i = i + 1
|
92
135
|
end
|
93
|
-
|
136
|
+
[views, placemarks]
|
94
137
|
end
|
95
138
|
|
96
139
|
end
|
140
|
+
|
141
|
+
## Example
|
142
|
+
#make_function_path(10,
|
143
|
+
# :latitude => Line.interpolate(38.8, 40.3),
|
144
|
+
# :altitude => Line.interpolate(10000, 2000),
|
145
|
+
# :heading => Line.interpolate(0, 90),
|
146
|
+
# :tilt => Line.interpolate(40.0, 90),
|
147
|
+
# :roll => 0,
|
148
|
+
# :show_placemarks => 1,
|
149
|
+
# :duration => Quadratic.interpolate(2.0, 4.0, 0.0, 1.0),
|
150
|
+
#) do |a, v|
|
151
|
+
# puts "callback here"
|
152
|
+
# if v.has_key? :callback_value then
|
153
|
+
# v[:callback_value] += 1
|
154
|
+
# else
|
155
|
+
# v[:pause] = 0.01
|
156
|
+
# v[:callback_value] = 1
|
157
|
+
# end
|
158
|
+
# puts v[:callback_value]
|
159
|
+
# v
|
160
|
+
#end
|
data/lib/kamelopard/helpers.rb
CHANGED
@@ -401,15 +401,21 @@
|
|
401
401
|
|
402
402
|
# Given a hash of values, this creates an AbstractView object. Possible
|
403
403
|
# values in the hash are :latitude, :longitude, :altitude, :altitudeMode,
|
404
|
-
# :tilt, :heading, :roll, and :
|
405
|
-
# object will result; otherwise, a LookAt object
|
406
|
-
# :roll and :range will still result in a Camera
|
407
|
-
#
|
408
|
-
#
|
404
|
+
# :tilt, :heading, :roll, :range, :begin, :end, and :when. If the hash
|
405
|
+
# specifies :roll, a Camera object will result; otherwise, a LookAt object
|
406
|
+
# will result. Specifying both :roll and :range will still result in a Camera
|
407
|
+
# object, and the :range option will be ignored.
|
408
|
+
#
|
409
|
+
# :begin, :end, and :when are used to create the view's timestamp or timespan
|
410
|
+
#
|
411
|
+
# :roll, :range, and the timestamp / timespan options have no default; all
|
412
|
+
# other values default to 0 except :altitudeMode, which defaults to
|
413
|
+
# :relativeToGround.
|
409
414
|
def make_view_from(options = {})
|
410
415
|
o = {}
|
411
416
|
o.merge! options
|
412
|
-
options.each do |k, v|
|
417
|
+
options.each do |k, v|
|
418
|
+
o[k.to_sym] = v unless k.kind_of? Symbol
|
413
419
|
end
|
414
420
|
|
415
421
|
# Set defaults
|
@@ -432,8 +438,18 @@
|
|
432
438
|
else
|
433
439
|
view = Kamelopard::LookAt.new p
|
434
440
|
end
|
441
|
+
|
442
|
+
if o.has_key? :when then
|
443
|
+
o[:timestamp] = Kamelopard::TimeStamp.new(o[:when])
|
444
|
+
elsif o.has_key? :begin or o.has_key? :end then
|
445
|
+
(b, e) = [nil, nil]
|
446
|
+
b = o[:begin] if o.has_key? :begin
|
447
|
+
e = o[:end] if o.has_key? :end
|
448
|
+
o[:timespan] = Kamelopard::TimeSpan.new(b, e)
|
449
|
+
end
|
435
450
|
|
436
|
-
[ :altitudeMode, :tilt, :heading, :
|
451
|
+
[ :altitudeMode, :tilt, :heading, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
|
452
|
+
#p o[a] if o.has_key? a and a == :timestamp
|
437
453
|
view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
|
438
454
|
end
|
439
455
|
|
@@ -596,3 +612,49 @@
|
|
596
612
|
def get_doc_holder
|
597
613
|
return Kamelopard::DocumentHolder.instance
|
598
614
|
end
|
615
|
+
|
616
|
+
# Generates a series of points in a path that will simulate Earth's FlyTo in
|
617
|
+
# bounce mode, from one view to another. Note that the view objects must be
|
618
|
+
# the same time: either LookAt, or Camera
|
619
|
+
#--
|
620
|
+
# XXX Fix the limitation that the views must be the same type
|
621
|
+
# XXX Make the height of the bounce relate to the distance of the travel
|
622
|
+
# XXX Make the direction of change for elements that cycle smart enough to
|
623
|
+
# choose the shortest direction around the circle
|
624
|
+
#++
|
625
|
+
def bounce(a, b, duration, points, options = {})
|
626
|
+
raise "Arguments to bounce() must either be Camera or LookAt objects, and must be the same type" unless
|
627
|
+
((a.kind_of? Kamelopard::Camera and b.kind_of? Kamelopard::Camera) or
|
628
|
+
(a.kind_of? Kamelopard::LookAt and b.kind_of? Kamelopard::LookAt))
|
629
|
+
# The idea here is just to generate a function; the hard bit is finding
|
630
|
+
# control points.
|
631
|
+
include Kamelopard
|
632
|
+
include Kamelopard::Functions
|
633
|
+
|
634
|
+
max_alt = a.altitude
|
635
|
+
max_alt = b.altitude if b.altitude > max_alt
|
636
|
+
|
637
|
+
opts = {
|
638
|
+
:latitude => Line.interpolate(a.latitude, b.latitude),
|
639
|
+
:longitude => Line.interpolate(a.longitude, b.longitude),
|
640
|
+
:heading => Line.interpolate(a.heading, b.heading),
|
641
|
+
:tilt => Line.interpolate(a.tilt, b.tilt),
|
642
|
+
# XXX This doesn't really work. An actual altitude requires a
|
643
|
+
# value, and a mode, and we ignore the modes because there's no
|
644
|
+
# way for us to figure out absolute altitudes given, say,
|
645
|
+
# :relativeToGround
|
646
|
+
:altitude => Quadratic.interpolate(a.altitude, b.altitude, 0.3 / 1.6, 1.3 * (b.altitude - a.altitude).abs),
|
647
|
+
# def self.interpolate(ymin, ymax, x1, y1, min = -1.0, max = 1.0)
|
648
|
+
:altitudeMode => a.altitudeMode,
|
649
|
+
:duration => duration * 1.0 / points,
|
650
|
+
}
|
651
|
+
opts[:no_flyto] = 1 if options.has_key?(:no_flyto)
|
652
|
+
opts[:show_placemarks] = 1 if options.has_key?(:show_placemarks)
|
653
|
+
|
654
|
+
if a.kind_of? Camera then
|
655
|
+
opts[:roll] = Line.interpolate(a.roll, b.roll)
|
656
|
+
else
|
657
|
+
opts[:range] = Line.interpolate(a.range, b.range)
|
658
|
+
end
|
659
|
+
return make_function_path(points, opts)
|
660
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
module Kamelopard
|
4
|
+
module Multicam
|
5
|
+
def self.cross_product(v1, v2)
|
6
|
+
x = ( (v1[1] * v2[2]) - (v1[2] * v2[1]) )
|
7
|
+
y = - ( (v1[0] * v2[2]) - (v1[2] * v2[0]) )
|
8
|
+
z = ( (v1[0] * v2[1]) - (v1[1] * v2[0]) )
|
9
|
+
return Vector[x, y, z]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.dotprod_angle(a, b, negate = false)
|
13
|
+
begin
|
14
|
+
d = 180.0 * (Math.acos(a.inner_product(b) / a.r / b.r)) / Math::PI
|
15
|
+
rescue
|
16
|
+
d = 0
|
17
|
+
#puts "argument was #{a.inner_product(b)}, from vectors #{a} and #{b}"
|
18
|
+
end
|
19
|
+
d = 0 if d.respond_to? :nan? and d.nan?
|
20
|
+
raise "#{a}, #{b}, #{a.inner_product(b)}" if d == Float::INFINITY
|
21
|
+
|
22
|
+
# Complicating factor: dot product goes from 0 to 180, not 0 to 360. We'll
|
23
|
+
# have to know whether to negate based on external input (like if the
|
24
|
+
# original angle involved was close to the threshold)
|
25
|
+
d = -d if negate
|
26
|
+
|
27
|
+
return d
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.rot_x(a)
|
31
|
+
# This, and the other two rotation matrix functions, must convert
|
32
|
+
# the angle to radians
|
33
|
+
a = a * Math::PI / 180.0
|
34
|
+
return Matrix[[1, 0, 0], [0, Math.cos(a), -1 * Math.sin(a)], [0, Math.sin(a), Math.cos(a)]]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.rot_y(a)
|
38
|
+
a = a * Math::PI / 180.0
|
39
|
+
return Matrix[[Math.cos(a), 0, Math.sin(a)], [0, 1, 0], [-1 * Math.sin(a), 0, Math.cos(a)]]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.rot_z(a)
|
43
|
+
a = a * Math::PI / 180.0
|
44
|
+
return Matrix[[Math.cos(a), -1 * Math.sin(a), 0], [Math.sin(a), Math.cos(a), 0], [0, 0, 1]]
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.same_quadrant(a, b)
|
48
|
+
(0..2).each do |i|
|
49
|
+
return false if (a[i] > 0 and b[i] < 0) or (a[i] < 0 and b[i] > 0)
|
50
|
+
end
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Vec is the camera vector. up_vec is the vector out the top of the camera
|
55
|
+
def self.vector_to_camera(vec, up_vec)
|
56
|
+
# The heading is the angle between two planes, the first formed by the
|
57
|
+
# camera vector and the original Z axis, and the second formed by the
|
58
|
+
# original Y and Z axes. This angle between two planes is the same
|
59
|
+
# as the angle between their two normals. The normal of the first
|
60
|
+
# plane is the cross product of the two vector, and that of the
|
61
|
+
# second is simply the X axis. The first cross product will be zero
|
62
|
+
# if the camera position vector is parallel to the Z axis; in that
|
63
|
+
# case we want the angle between the up vector and the y axis
|
64
|
+
# This will only find values up to 180 degrees, and won't
|
65
|
+
# distinguish direction correctly. For that, look at the Z
|
66
|
+
# component of the cross product of the two normals.
|
67
|
+
cam_z_norm = cross_product(vec, Vector[0,0,-1])
|
68
|
+
if cam_z_norm.r == 0
|
69
|
+
heading = dotprod_angle(up_vec, Vector[0,1,0],
|
70
|
+
(cross_product(up_vec, Vector[0,1,0])[2] > 0))
|
71
|
+
else
|
72
|
+
heading = dotprod_angle(cam_z_norm, Vector[1,0,0],
|
73
|
+
(cross_product(cam_z_norm, Vector[1,0,0])[2] > 0))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Tilt is calculated from the vector alone, and is the angle between it and
|
77
|
+
# the original Z axis, calculated via the dot product
|
78
|
+
tilt = dotprod_angle(vec, Vector[0,0,1])
|
79
|
+
|
80
|
+
# For roll, take the original UP vector, and now that I've got
|
81
|
+
# valid heading and tilt, transform it by those values. Take the
|
82
|
+
# angle between it and my current UP vector. Make it negative if
|
83
|
+
# their cross product, up vector first, isn't the same direction as
|
84
|
+
# the camera vector.
|
85
|
+
transformed_up = rot_z(heading) * rot_x(tilt) * Vector[0,1,0]
|
86
|
+
if cross_product(up_vec, transformed_up).r == 0
|
87
|
+
negate = not(same_quadrant(up_vec, transformed_up))
|
88
|
+
else
|
89
|
+
negate = same_quadrant(vec, cross_product(up_vec, transformed_up))
|
90
|
+
end
|
91
|
+
roll = dotprod_angle(up_vec, transformed_up, negate)
|
92
|
+
|
93
|
+
return [heading, tilt, roll]
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.make_placemark(name, lat, lon, alt, tilt, roll, heading)
|
97
|
+
p = point(lon, lat, alt, :relativeToGround)
|
98
|
+
l = camera p, :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :relativeToGround
|
99
|
+
pl = placemark(name, :geometry => p, :abstractView => l)
|
100
|
+
f = get_folder
|
101
|
+
f << pl
|
102
|
+
end
|
103
|
+
|
104
|
+
# XXX Change / augment API, so that this function, or a cognate, takes
|
105
|
+
# a view, and returns a view modified for the camera in question
|
106
|
+
def self.get_camera(heading, tilt, roll, cam_num, cam_angle, cam_count = nil)
|
107
|
+
if cam_angle.nil? then
|
108
|
+
cam_angle = cam_num * 360.0 / cam_count
|
109
|
+
else
|
110
|
+
cam_angle = cam_angle * cam_num
|
111
|
+
end
|
112
|
+
# The camera vector is [0,0,1] rotated around the Y axis the amount
|
113
|
+
# of the camera angle
|
114
|
+
camera = rot_y(cam_angle) * Vector[0,0,1]
|
115
|
+
|
116
|
+
# The up vector is the same for all cameras
|
117
|
+
up = Vector[0,1,0]
|
118
|
+
matrix = rot_z(heading) * rot_x(tilt) * rot_z(roll)
|
119
|
+
(h, t, r) = vector_to_camera(matrix * camera, matrix * up)
|
120
|
+
# XXX What am I getting wrong, to require the negated roll?
|
121
|
+
return [h, t, -1 * r]
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.test(kml_name = 'multicam_test.kml')
|
125
|
+
name_document 'tourvid'
|
126
|
+
get_document().open = 1
|
127
|
+
|
128
|
+
[:roll, :tilt, :heading].each do |which|
|
129
|
+
camera = Vector[0,0,1]
|
130
|
+
heading = 0
|
131
|
+
tilt = 45
|
132
|
+
roll = 0
|
133
|
+
lat = 40
|
134
|
+
lon = -111
|
135
|
+
alt = 100
|
136
|
+
|
137
|
+
puts "------------------"
|
138
|
+
puts "Running #{which}"
|
139
|
+
folder which.to_s
|
140
|
+
get_folder().open = 1
|
141
|
+
|
142
|
+
up = Vector[0,1,0]
|
143
|
+
(0..36).each do |i|
|
144
|
+
if which == :roll then
|
145
|
+
roll = -180 + i * 10
|
146
|
+
heading = 23
|
147
|
+
elsif which == :heading then
|
148
|
+
heading = i * 10
|
149
|
+
heading = heading - 360 if heading >= 180
|
150
|
+
else
|
151
|
+
tilt = i * 5
|
152
|
+
end
|
153
|
+
|
154
|
+
# This has been verified visually as the right matrix
|
155
|
+
matrix = rot_z(heading) * rot_x(tilt) * rot_z(roll)
|
156
|
+
|
157
|
+
trans_up = matrix * up
|
158
|
+
trans_cam = matrix * camera
|
159
|
+
trans_cross = cross_product(trans_cam, trans_up)
|
160
|
+
(screen_head, screen_tilt, screen_roll) = vector_to_camera(trans_cam, trans_up)
|
161
|
+
|
162
|
+
diff_limit = 3
|
163
|
+
a = dotprod_angle(trans_up, trans_cam)
|
164
|
+
if ((heading - screen_head).abs > diff_limit or (tilt - screen_tilt).abs > diff_limit or (roll - screen_roll).abs > diff_limit) then
|
165
|
+
#if which == :roll then
|
166
|
+
puts " PLACEMARK #{i}"
|
167
|
+
# puts " Camera vector: #{trans_cam}, mag: #{trans_cam.r}"
|
168
|
+
# puts " Up vector: #{trans_up}, mag: #{trans_up.r}"
|
169
|
+
# puts " Cross prod: #{trans_cross}, mag: #{trans_cross.r}"
|
170
|
+
puts " UpZ: #{trans_up[2]}"
|
171
|
+
puts " Orig H/T/R: #{heading}/#{tilt}/#{roll}"
|
172
|
+
puts " Screen H/T/R: #{screen_head}/#{screen_tilt}/#{screen_roll}"
|
173
|
+
end
|
174
|
+
make_placemark(i, lat, lon, alt, screen_tilt, screen_roll, screen_head)
|
175
|
+
end
|
176
|
+
puts
|
177
|
+
write_kml_to kml_name
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/lib/kamelopard/spline.rb
CHANGED
@@ -2,56 +2,88 @@
|
|
2
2
|
require 'matrix'
|
3
3
|
|
4
4
|
# Basic support for splines
|
5
|
-
|
6
5
|
module Kamelopard
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
6
|
+
module Functions
|
7
|
+
class SplineFunction < FunctionMultiDim
|
8
|
+
attr_reader :control_points, :total_dur, :tension
|
9
|
+
|
10
|
+
def initialize(ndims, tension = 0.5)
|
11
|
+
@ndims = ndims
|
12
|
+
@control_points = []
|
13
|
+
@total_dur = 0
|
14
|
+
@tension = tension
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds a new control point. :dur is a way of indicating the
|
19
|
+
# duration of the journey from the last point to this one, and is
|
20
|
+
# ignored for the first control point in the spline. Values for
|
21
|
+
# :dur are in whatever units the user wants; a spline with three
|
22
|
+
# control points with durations of 0, 10, and 20 will be identical
|
23
|
+
# to one with durations of 0, 1, and 2.
|
24
|
+
def add_control_point(point, dur)
|
25
|
+
@total_dur = @total_dur + dur if @control_points.size > 0
|
26
|
+
@control_points << [ point, dur ]
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_function(x)
|
30
|
+
# X will be between 0 and 1
|
31
|
+
# Find which control points I should am using for the point in
|
32
|
+
# question
|
33
|
+
|
34
|
+
dur = 0
|
35
|
+
last_dur = 0
|
36
|
+
cur_i = 0
|
37
|
+
u = 0
|
38
|
+
@control_points.each_index do |i|
|
39
|
+
next if i == 0
|
40
|
+
cur_i = i
|
41
|
+
last_dur = dur
|
42
|
+
if 1.0 * (dur + @control_points[i][1]) / @total_dur >= x then
|
43
|
+
# I've found the correct two control points: cp[i-1] and cp[i]
|
44
|
+
# u is the point on the interval between the two control points
|
45
|
+
# that we're interested in. 0 would be the first control point,
|
46
|
+
# and 1 the second
|
47
|
+
u = (x * @total_dur - dur) / @control_points[i][1]
|
48
|
+
break
|
49
|
+
end
|
50
|
+
dur = dur + @control_points[i][1]
|
51
|
+
end
|
52
|
+
|
53
|
+
# http://www.cs.cmu.edu/~462/projects/assn2/assn2/catmullRom.pdf
|
54
|
+
|
55
|
+
# cp = control points. cur_i will be at least 1
|
56
|
+
# I need two control points on either side of this part of the
|
57
|
+
# spline. If they don't exist, duplicate the endpoints of the
|
58
|
+
# control points.
|
59
|
+
cp1 = @control_points[cur_i-1][0]
|
60
|
+
cp2 = @control_points[cur_i][0]
|
61
|
+
if cur_i == 1 then
|
62
|
+
cpt1 = cp1
|
63
|
+
else
|
64
|
+
cpt1 = @control_points[cur_i-2][0]
|
65
|
+
end
|
66
|
+
if cur_i >= @control_points.size - 1 then
|
67
|
+
cpt2 = cp2
|
68
|
+
else
|
69
|
+
cpt2 = @control_points[cur_i+1][0]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Can't just say Matrix[cp], because that adds an extra
|
73
|
+
# dimension to the matrix, somehow.
|
74
|
+
cps = Matrix[cpt1, cp1, cp2, cpt2]
|
75
|
+
|
76
|
+
t = @tension
|
77
|
+
h = Matrix[
|
78
|
+
[ 0, 1, 0, 0 ],
|
79
|
+
[ -t, 0, t, 0 ],
|
80
|
+
[ 2*t, t-3, 3-2*t, -t ],
|
81
|
+
[ -t, 2-t, t-2, t]
|
82
|
+
]
|
83
|
+
|
84
|
+
p = Matrix[[1, u, u**2, u**3]] * h * cps
|
85
|
+
return p
|
51
86
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
result
|
56
|
-
end
|
57
|
-
end
|
87
|
+
end ## End of SplineFunction class
|
88
|
+
end ## End of Function module
|
89
|
+
end ## End of Kamelopard module
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kamelopard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-06-18 00:00:00.000000000 Z
|
14
14
|
dependencies: []
|
15
15
|
description: Various classes and functions used to ease development of KML files,
|
16
16
|
in particular for development of Google Earth tours
|
@@ -21,6 +21,7 @@ executables: []
|
|
21
21
|
extensions: []
|
22
22
|
extra_rdoc_files: []
|
23
23
|
files:
|
24
|
+
- lib/kamelopard/multicam.rb
|
24
25
|
- lib/kamelopard/geocode.rb
|
25
26
|
- lib/kamelopard/classes.rb
|
26
27
|
- lib/kamelopard/helpers.rb
|