propane 0.3.0.pre-java
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.
- 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
|