propane 0.3.0.pre-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.mvn/extensions.xml +8 -0
- data/.mvn/wrapper/maven-wrapper.properties +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +69 -0
- data/Rakefile +59 -0
- data/VERSION.txt +4 -0
- data/bin/propane +8 -0
- data/examples/complete/Rakefile +32 -0
- data/examples/complete/data/Texture01.jpg +0 -0
- data/examples/complete/data/Texture02.jpg +0 -0
- data/examples/complete/data/Univers45.vlw +0 -0
- data/examples/complete/data/displaceFrag.glsl +8 -0
- data/examples/complete/data/displaceVert.glsl +201 -0
- data/examples/complete/glsl_heightmap_noise.rb +121 -0
- data/examples/complete/kinetic_type.rb +79 -0
- data/examples/regular/Rakefile +30 -0
- data/examples/regular/arcball_box.rb +36 -0
- data/examples/regular/creating_colors.rb +57 -0
- data/examples/regular/elegant_ball.rb +159 -0
- data/examples/regular/flight_patterns.rb +63 -0
- data/examples/regular/grey_circles.rb +28 -0
- data/examples/regular/jwishy.rb +100 -0
- data/examples/regular/letters.rb +42 -0
- data/examples/regular/lib/boundary.rb +38 -0
- data/examples/regular/lib/particle.rb +77 -0
- data/examples/regular/lib/particle_system.rb +111 -0
- data/examples/regular/liquidy.rb +40 -0
- data/examples/regular/mouse_button_demo.rb +34 -0
- data/examples/regular/polyhedrons.rb +248 -0
- data/examples/regular/ribbon_doodle.rb +89 -0
- data/examples/regular/vector_math.rb +36 -0
- data/examples/regular/words.rb +41 -0
- data/lib/PROCESSING_LICENSE.txt +456 -0
- data/lib/export.txt +10 -0
- data/lib/propane.rb +12 -0
- data/lib/propane/app.rb +197 -0
- data/lib/propane/helper_methods.rb +177 -0
- data/lib/propane/helpers/numeric.rb +9 -0
- data/lib/propane/library_loader.rb +117 -0
- data/lib/propane/runner.rb +88 -0
- data/lib/propane/underscorer.rb +19 -0
- data/lib/propane/version.rb +5 -0
- data/library/boids/boids.rb +201 -0
- data/library/control_panel/control_panel.rb +172 -0
- data/pom.rb +113 -0
- data/pom.xml +198 -0
- data/propane.gemspec +28 -0
- data/src/monkstone/ColorUtil.java +67 -0
- data/src/monkstone/MathTool.java +195 -0
- data/src/monkstone/PropaneLibrary.java +47 -0
- data/src/monkstone/core/AbstractLibrary.java +102 -0
- data/src/monkstone/fastmath/Deglut.java +115 -0
- data/src/monkstone/vecmath/AppRender.java +87 -0
- data/src/monkstone/vecmath/JRender.java +56 -0
- data/src/monkstone/vecmath/ShapeRender.java +87 -0
- data/src/monkstone/vecmath/vec2/Vec2.java +670 -0
- data/src/monkstone/vecmath/vec3/Vec3.java +708 -0
- data/test/respond_to_test.rb +208 -0
- data/vendors/Rakefile +48 -0
- metadata +130 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
require "#{PROPANE_ROOT}/lib/propane"
|
4
|
+
require "#{PROPANE_ROOT}/lib/propane/app"
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module Propane
|
8
|
+
# Utility class to handle the different commands that the 'rp5' command
|
9
|
+
# offers. Able to run, watch, live, create, app, and unpack
|
10
|
+
class Runner
|
11
|
+
attr_reader :options, :name
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@options = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Start running a propane sketch from the passed-in arguments
|
18
|
+
def self.execute
|
19
|
+
runner = new
|
20
|
+
runner.parse_options(ARGV)
|
21
|
+
runner.execute!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Dispatch central.
|
25
|
+
def execute!
|
26
|
+
show_help if options.empty?
|
27
|
+
show_version if options[:version]
|
28
|
+
run_sketch if options[:run]
|
29
|
+
install if options[:install]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Parse the command-line options. Keep it simple.
|
33
|
+
def parse_options(args)
|
34
|
+
opt_parser = OptionParser.new do |opts|
|
35
|
+
# Set a banner, displayed at the top
|
36
|
+
# of the help screen.
|
37
|
+
opts.banner = 'Usage: propane [options] sketch.rb'
|
38
|
+
|
39
|
+
# Define the options, and what they do
|
40
|
+
options[:version] = false
|
41
|
+
opts.on('-v', '--version', 'Propane Version') do
|
42
|
+
options[:version] = true
|
43
|
+
end
|
44
|
+
|
45
|
+
options[:install] = false
|
46
|
+
opts.on('-i', '--install', 'Installs jruby-complete') do
|
47
|
+
options[:install] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
options[:run] = false
|
51
|
+
opts.on('-r', '--run', 'Run the sketch using jruby-complete') do
|
52
|
+
options[:run] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
# This displays the help screen, all programs are
|
56
|
+
# assumed to have this option.
|
57
|
+
opts.on('-h', '--help', 'Display this screen') do
|
58
|
+
puts opts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
@name = opt_parser.parse(args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def run_sketch
|
66
|
+
root = File.absolute_path(File.dirname(ARGV.shift))
|
67
|
+
sketch = File.join(root, name)
|
68
|
+
warn_format = 'File %s does not not Exist!'
|
69
|
+
return warn(format(warn_format, sketch)) unless File.exist?(sketch)
|
70
|
+
command = [
|
71
|
+
'java',
|
72
|
+
'-cp',
|
73
|
+
"#{PROPANE_ROOT}/lib/ruby/jruby-complete.jar",
|
74
|
+
'org.jruby.Main',
|
75
|
+
sketch.to_s
|
76
|
+
].flatten
|
77
|
+
exec(*command)
|
78
|
+
end
|
79
|
+
|
80
|
+
def show_version
|
81
|
+
puts format('Propane version %s', Propane::VERSION)
|
82
|
+
end
|
83
|
+
|
84
|
+
def install
|
85
|
+
system "cd #{PROPANE_ROOT}/vendors && rake"
|
86
|
+
end
|
87
|
+
end # class Runner
|
88
|
+
end # module Propane
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
module Propane
|
4
|
+
# This class defines a single method that converts a method name
|
5
|
+
# from camel or mixed case to snake case.
|
6
|
+
#
|
7
|
+
class Underscorer
|
8
|
+
# Underscorer.("CamelCase") => "camel_case"
|
9
|
+
#
|
10
|
+
def self.call(input)
|
11
|
+
string = input.to_s
|
12
|
+
string.gsub(/::/, '/')
|
13
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
14
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
15
|
+
.tr('-', '_')
|
16
|
+
.downcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# Boids -- after Tom de Smedt.
|
2
|
+
# See his Python version: http://nodebox.net/code/index.php/Boids
|
3
|
+
# This is an example of how a pure-Ruby library can work. Original for
|
4
|
+
# ruby-processing Jeremy Ashkenas. Reworked, re-factored for JRubyArt 0.9+
|
5
|
+
# by Martin Prout, features forwardable, keyword args, Vec3D and Vec2D.
|
6
|
+
class Boid
|
7
|
+
attr_accessor :boids, :pos, :vel, :is_perching, :perch_time
|
8
|
+
|
9
|
+
def initialize(boids, pos)
|
10
|
+
@boids, @flock = boids, boids
|
11
|
+
@pos = pos
|
12
|
+
@vel = Vec3D.new
|
13
|
+
@is_perching = false
|
14
|
+
@perch_time = 0.0
|
15
|
+
end
|
16
|
+
|
17
|
+
def cohesion(d:)
|
18
|
+
# Boids gravitate towards the center of the flock,
|
19
|
+
# Which is the averaged position of the rest of the boids.
|
20
|
+
vect = Vec3D.new
|
21
|
+
@boids.each do |boid|
|
22
|
+
vect += boid.pos unless boid == self
|
23
|
+
end
|
24
|
+
count = @boids.length - 1.0
|
25
|
+
vect /= count
|
26
|
+
(vect - pos) / d
|
27
|
+
end
|
28
|
+
|
29
|
+
def separation(radius:)
|
30
|
+
# Boids don't like to cuddle.
|
31
|
+
vect = Vec3D.new
|
32
|
+
@boids.each do |boid|
|
33
|
+
if boid != self
|
34
|
+
dv = pos - boid.pos
|
35
|
+
vect += dv if dv.mag < radius
|
36
|
+
end
|
37
|
+
end
|
38
|
+
vect
|
39
|
+
end
|
40
|
+
|
41
|
+
def alignment(d:)
|
42
|
+
# Boids like to fly at the speed of traffic.
|
43
|
+
vect = Vec3D.new
|
44
|
+
@boids.each do |boid|
|
45
|
+
vect += boid.vel if boid != self
|
46
|
+
end
|
47
|
+
count = @boids.length - 1.0
|
48
|
+
vect /= count
|
49
|
+
(vect - vel) / d
|
50
|
+
end
|
51
|
+
|
52
|
+
def limit(max:)
|
53
|
+
# Tweet, Tweet! The boid police will bust you for breaking the speed limit.
|
54
|
+
most = [vel.x.abs, vel.y.abs, vel.z.abs].max
|
55
|
+
return if most < max
|
56
|
+
scale = max / most.to_f
|
57
|
+
@vel *= scale
|
58
|
+
end
|
59
|
+
|
60
|
+
def angle
|
61
|
+
Vec2D.new(vel.x, vel.y).heading
|
62
|
+
end
|
63
|
+
|
64
|
+
def goal(target, d = 50.0)
|
65
|
+
# Them boids is hungry.
|
66
|
+
(target - pos) / d
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
require 'forwardable'
|
71
|
+
|
72
|
+
# The Boids class
|
73
|
+
class Boids
|
74
|
+
include Enumerable
|
75
|
+
extend Forwardable
|
76
|
+
def_delegators(:@boids, :reject, :<<, :each, :shuffle!, :length, :next)
|
77
|
+
|
78
|
+
attr_reader :has_goal, :perch, :perch_tm, :perch_y
|
79
|
+
|
80
|
+
def initialize
|
81
|
+
@boids = []
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.flock(n:, x:, y:, w:, h:)
|
85
|
+
flock = Boids.new.setup(n, x, y, w, h)
|
86
|
+
flock.goal(target: Vec3D.new(w / 2, h / 2, 0))
|
87
|
+
end
|
88
|
+
|
89
|
+
def setup(n, x, y, w, h)
|
90
|
+
n.times do
|
91
|
+
dx, dy = rand(w), rand(h)
|
92
|
+
z = rand(200.0)
|
93
|
+
self << Boid.new(self, Vec3D.new(x + dx, y + dy, z))
|
94
|
+
end
|
95
|
+
@x, @y, @w, @h = x, y, w, h
|
96
|
+
@scattered = false
|
97
|
+
@scatter = 0.005
|
98
|
+
@scatter_time = 50.0
|
99
|
+
@scatter_i = 0.0
|
100
|
+
@perch = 1.0 # Lower this number to divebomb.
|
101
|
+
@perch_y = h
|
102
|
+
@perch_tm = -> { 25.0 + rand(50.0) }
|
103
|
+
@has_goal = false
|
104
|
+
@flee = false
|
105
|
+
@goal = Vec3D.new
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def scatter(chance = 0.005, frames = 50.0)
|
110
|
+
@scatter = chance
|
111
|
+
@scatter_time = frames
|
112
|
+
end
|
113
|
+
|
114
|
+
def no_scatter
|
115
|
+
@scatter = 0.0
|
116
|
+
end
|
117
|
+
|
118
|
+
def perch(ground = nil, chance = 1.0, frames = nil)
|
119
|
+
@perch_tm = frames.nil? ? -> { 25.0 + rand(50.0) } : frames
|
120
|
+
@perch_y = ground.nil? ? @h : ground
|
121
|
+
@perch = chance
|
122
|
+
end
|
123
|
+
|
124
|
+
def no_perch
|
125
|
+
@perch = 0.0
|
126
|
+
end
|
127
|
+
|
128
|
+
def goal(target:, flee: false)
|
129
|
+
@has_goal = true
|
130
|
+
@flee = flee
|
131
|
+
@goal = target
|
132
|
+
self
|
133
|
+
end
|
134
|
+
|
135
|
+
def no_goal
|
136
|
+
@has_goal = false
|
137
|
+
end
|
138
|
+
|
139
|
+
def constrain
|
140
|
+
# Put them boids in a cage.
|
141
|
+
dx, dy = @w * 0.1, @h * 0.1
|
142
|
+
each do |b|
|
143
|
+
b.vel.x += rand(dx) if b.pos.x < @x - dx
|
144
|
+
b.vel.x += rand(dy) if b.pos.y < @y - dy
|
145
|
+
b.vel.x -= rand(dx) if b.pos.x > @x + @w + dx
|
146
|
+
b.vel.y -= rand(dy) if b.pos.y > @y + @h + dy
|
147
|
+
b.vel.z += 10.0 if b.pos.z < 0.0
|
148
|
+
b.vel.z -= 10.0 if b.pos.z > 100.0
|
149
|
+
next unless b.pos.y > perch_y && rand < perch
|
150
|
+
b.pos.y = perch_y
|
151
|
+
b.vel.y = -(b.vel.y.abs) * 0.2
|
152
|
+
b.is_perching = true
|
153
|
+
b.perch_time = perch_tm.respond_to?(:call) ? perch_tm.call : perch_tm
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def update(goal: 20.0, limit: 30.0, **args)
|
158
|
+
shuffled = args.fetch(:shuffled, true)
|
159
|
+
cohesion = args.fetch(:cohesion, 100)
|
160
|
+
separation = args.fetch(:separation, 10)
|
161
|
+
alignment = args.fetch(:alignment, 5.0)
|
162
|
+
# Just flutter, little boids ... just flutter away.
|
163
|
+
# Shuffling keeps things flowing smooth.
|
164
|
+
shuffle! if shuffled
|
165
|
+
m1 = 1.0 # cohesion
|
166
|
+
m2 = 1.0 # separation
|
167
|
+
m3 = 1.0 # alignment
|
168
|
+
m4 = 1.0 # goal
|
169
|
+
@scattered = true if !(@scattered) && rand < @scatter
|
170
|
+
if @scattered
|
171
|
+
m1 = -m1
|
172
|
+
m3 *= 0.25
|
173
|
+
@scatter_i += 1.0
|
174
|
+
end
|
175
|
+
if @scatter_i >= @scatter_time
|
176
|
+
@scattered = false
|
177
|
+
@scatter_i = 0.0
|
178
|
+
end
|
179
|
+
m4 = 0.0 unless has_goal
|
180
|
+
m4 = -m4 if @flee
|
181
|
+
each do |b|
|
182
|
+
if b.is_perching
|
183
|
+
if b.perch_time > 0.0
|
184
|
+
b.perch_time -= 1.0
|
185
|
+
next
|
186
|
+
else
|
187
|
+
b.is_perching = false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
v1 = b.cohesion(d: cohesion)
|
191
|
+
v2 = b.separation(radius: separation)
|
192
|
+
v3 = b.alignment(d: alignment)
|
193
|
+
v4 = b.goal(@goal, goal)
|
194
|
+
# NB: vector must precede scalar in '*' operation below
|
195
|
+
b.vel += (v1 * m1 + v2 * m2 + v3 * m3 + v4 * m4)
|
196
|
+
b.limit(max: limit)
|
197
|
+
b.pos += b.vel
|
198
|
+
end
|
199
|
+
constrain
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# Here's a little library for quickly hooking up controls to sketches.
|
2
|
+
# For messing with the parameters and such.
|
3
|
+
# These controls will set instance variables on the sketches.
|
4
|
+
|
5
|
+
# You can make sliders, checkboxes, buttons, and drop-down menus.
|
6
|
+
# (optionally) pass the range and default value.
|
7
|
+
|
8
|
+
module ControlPanel
|
9
|
+
# class used to create slider elements for control_panel
|
10
|
+
class Slider < javax.swing.JSlider
|
11
|
+
def initialize(control_panel, name, range, initial_value, proc = nil)
|
12
|
+
min = range.begin * 100
|
13
|
+
max = (
|
14
|
+
(range.exclude_end? && range.begin.respond_to?(:succ)) ?
|
15
|
+
range.max : range.end) * 100
|
16
|
+
super(min, max)
|
17
|
+
set_minor_tick_spacing((max - min).abs / 10)
|
18
|
+
set_paint_ticks true
|
19
|
+
# paint_labels = true
|
20
|
+
set_preferred_size(java.awt.Dimension.new(190, 30))
|
21
|
+
label = control_panel.add_element(self, name)
|
22
|
+
add_change_listener do
|
23
|
+
update_label(label, name, value)
|
24
|
+
$app.instance_variable_set("@#{name}", value) unless value.nil?
|
25
|
+
proc.call(value) if proc
|
26
|
+
end
|
27
|
+
set_value(initial_value ? initial_value * 100 : min)
|
28
|
+
fire_state_changed
|
29
|
+
end
|
30
|
+
|
31
|
+
def value
|
32
|
+
get_value / 100.0
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_label(label, name, value)
|
36
|
+
value = value.to_s
|
37
|
+
value << '0' if value.length < 4
|
38
|
+
label.set_text "<html><br><b>#{name}: #{value}</b></html>"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# class used to combo_box menu elements for control_panel
|
43
|
+
class Menu < javax.swing.JComboBox
|
44
|
+
def initialize(control_panel, name, elements, initial_value, proc = nil)
|
45
|
+
super(elements.to_java(:String))
|
46
|
+
set_preferred_size(java.awt.Dimension.new(190, 30))
|
47
|
+
control_panel.add_element(self, name)
|
48
|
+
add_action_listener do
|
49
|
+
$app.instance_variable_set("@#{name}", value) unless value.nil?
|
50
|
+
proc.call(value) if proc
|
51
|
+
end
|
52
|
+
set_selected_index(initial_value ? elements.index(initial_value) : 0)
|
53
|
+
end
|
54
|
+
|
55
|
+
def value
|
56
|
+
get_selected_item
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates check-box elements for control_panel
|
61
|
+
class Checkbox < javax.swing.JCheckBox
|
62
|
+
def initialize(control_panel, name, proc = nil)
|
63
|
+
@control_panel = control_panel
|
64
|
+
super(name.to_s)
|
65
|
+
set_preferred_size(java.awt.Dimension.new(190, 64))
|
66
|
+
set_horizontal_alignment javax.swing.SwingConstants::CENTER
|
67
|
+
control_panel.add_element(self, name, false)
|
68
|
+
add_action_listener do
|
69
|
+
$app.instance_variable_set("@#{name}", value) unless value.nil?
|
70
|
+
proc.call(value) if proc
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def value
|
75
|
+
is_selected
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creates button elements for control_panel
|
80
|
+
class Button < javax.swing.JButton
|
81
|
+
def initialize(control_panel, name, proc = nil)
|
82
|
+
super(name.to_s)
|
83
|
+
set_preferred_size(java.awt.Dimension.new(170, 64))
|
84
|
+
control_panel.add_element(self, name, false, true)
|
85
|
+
add_action_listener do
|
86
|
+
$app.send(name.to_s)
|
87
|
+
proc.call(value) if proc
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# class used to contain control_panel elements
|
93
|
+
class Panel < javax.swing.JFrame
|
94
|
+
java_import javax.swing.UIManager
|
95
|
+
|
96
|
+
attr_accessor :elements, :panel
|
97
|
+
|
98
|
+
def initialize
|
99
|
+
super()
|
100
|
+
@elements = []
|
101
|
+
@panel = javax.swing.JPanel.new(java.awt.FlowLayout.new(1, 0, 0))
|
102
|
+
set_feel
|
103
|
+
end
|
104
|
+
|
105
|
+
def display
|
106
|
+
add panel
|
107
|
+
set_size 200, 30 + (64 * elements.size)
|
108
|
+
set_default_close_operation javax.swing.JFrame::HIDE_ON_CLOSE
|
109
|
+
set_resizable false
|
110
|
+
set_location($app.width + 10, 0) unless $app.width + 10 > $app.displayWidth
|
111
|
+
panel.visible = true
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_element(element, name, has_label = true, _button_ = false)
|
115
|
+
if has_label
|
116
|
+
label = javax.swing.JLabel.new("<html><br><b>#{name}</b></html>")
|
117
|
+
panel.add label
|
118
|
+
end
|
119
|
+
elements << element
|
120
|
+
panel.add element
|
121
|
+
has_label ? label : nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def remove
|
125
|
+
remove_all
|
126
|
+
dispose
|
127
|
+
end
|
128
|
+
|
129
|
+
def slider(name, range = 0..100, initial_value = nil, &block)
|
130
|
+
Slider.new(self, name, range, initial_value, block || nil)
|
131
|
+
end
|
132
|
+
|
133
|
+
def menu(name, elements, initial_value = nil, &block)
|
134
|
+
Menu.new(self, name, elements, initial_value, block || nil)
|
135
|
+
end
|
136
|
+
|
137
|
+
def checkbox(name, initial_value = nil, &block)
|
138
|
+
checkbox = Checkbox.new(self, name, block || nil)
|
139
|
+
checkbox.do_click if initial_value == true
|
140
|
+
end
|
141
|
+
|
142
|
+
def button(name, &block)
|
143
|
+
Button.new(self, name, block || nil)
|
144
|
+
end
|
145
|
+
|
146
|
+
def look_feel(lf)
|
147
|
+
set_feel(lf)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def set_feel(lf = 'metal')
|
153
|
+
lafinfo = javax.swing.UIManager.getInstalledLookAndFeels
|
154
|
+
laf = lafinfo.select do |info|
|
155
|
+
info.getName.eql? lf.capitalize
|
156
|
+
end
|
157
|
+
javax.swing.UIManager.setLookAndFeel(laf[0].getClassName)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# instance methods module
|
162
|
+
module InstanceMethods
|
163
|
+
def control_panel
|
164
|
+
@control_panel ||= ControlPanel::Panel.new
|
165
|
+
return @control_panel unless block_given?
|
166
|
+
yield(@control_panel)
|
167
|
+
@control_panel.display
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
Propane::App.send :include, ControlPanel::InstanceMethods
|