kamelopard 0.0.11 → 0.0.12
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/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
|