processing 0.5.29 → 0.5.31
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/release-gem.yml +6 -6
- data/.github/workflows/test-draw.yml +33 -0
- data/.github/workflows/test.yml +4 -2
- data/ChangeLog.md +18 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +35 -0
- data/Rakefile +23 -0
- data/VERSION +1 -1
- data/lib/processing/all.rb +1 -0
- data/lib/processing/graphics_context.rb +240 -16
- data/lib/processing/shape.rb +183 -0
- data/lib/processing/vector.rb +3 -3
- data/processing.gemspec +4 -8
- data/test/helper.rb +100 -5
- data/test/p5.rb +76 -0
- data/test/test_graphics.rb +2 -8
- data/test/test_graphics_context.rb +451 -5
- data/test/test_shape.rb +387 -0
- data/test/test_vector.rb +1 -1
- metadata +17 -52
@@ -0,0 +1,183 @@
|
|
1
|
+
module Processing
|
2
|
+
|
3
|
+
|
4
|
+
# Shape object.
|
5
|
+
#
|
6
|
+
class Shape
|
7
|
+
|
8
|
+
# @private
|
9
|
+
def initialize(polygon = nil, children = nil, context: nil)
|
10
|
+
@polygon, @children = polygon, children
|
11
|
+
@context = context || Context.context__
|
12
|
+
@visible, @matrix = true, nil
|
13
|
+
@mode = @points = @closed = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Gets width of shape.
|
17
|
+
#
|
18
|
+
# @return [Numeric] width of shape
|
19
|
+
#
|
20
|
+
def width()
|
21
|
+
polygon = getInternal__ or return 0
|
22
|
+
(@bounds ||= polygon.bounds).width
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gets height of shape.
|
26
|
+
#
|
27
|
+
# @return [Numeric] height of shape
|
28
|
+
#
|
29
|
+
def height()
|
30
|
+
polygon = getInternal__ or return 0
|
31
|
+
(@bounds ||= polygon.bounds).height
|
32
|
+
end
|
33
|
+
|
34
|
+
alias w width
|
35
|
+
alias h height
|
36
|
+
|
37
|
+
# Returns whether the shape is visible or not.
|
38
|
+
#
|
39
|
+
# @return [Boolean] visible or not
|
40
|
+
#
|
41
|
+
def isVisible()
|
42
|
+
@visible
|
43
|
+
end
|
44
|
+
|
45
|
+
alias visible? isVisible
|
46
|
+
|
47
|
+
# Sets whether to display the shape or not.
|
48
|
+
#
|
49
|
+
# @return [nil] nil
|
50
|
+
#
|
51
|
+
def setVisible(visible)
|
52
|
+
@visible = !!visible
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def beginShape(mode = nil)
|
57
|
+
@mode = mode
|
58
|
+
@points ||= []
|
59
|
+
@polygon = nil# clear cache
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def endShape(close = nil)
|
64
|
+
raise "endShape() must be called after beginShape()" unless @points
|
65
|
+
@closed = close == GraphicsContext::CLOSE
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def vertex(x, y)
|
70
|
+
raise "vertex() must be called after beginShape()" unless @points
|
71
|
+
@points << x << y
|
72
|
+
end
|
73
|
+
|
74
|
+
def setVertex(index, point)
|
75
|
+
return nil unless @points && @points[index * 2, 2]&.size == 2
|
76
|
+
@points[index * 2, 2] = [point.x, point.y]
|
77
|
+
end
|
78
|
+
|
79
|
+
def getVertex(index)
|
80
|
+
return nil unless @points
|
81
|
+
point = @points[index * 2, 2]
|
82
|
+
return nil unless point&.size == 2
|
83
|
+
@context.createVector(*point)
|
84
|
+
end
|
85
|
+
|
86
|
+
def getVertexCount()
|
87
|
+
return 0 unless @points
|
88
|
+
@points.size / 2
|
89
|
+
end
|
90
|
+
|
91
|
+
def addChild(child)
|
92
|
+
return unless @children
|
93
|
+
@children.push child
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def getChild(index)
|
98
|
+
@children&.[](index)
|
99
|
+
end
|
100
|
+
|
101
|
+
def getChildCount()
|
102
|
+
@children&.size || 0
|
103
|
+
end
|
104
|
+
|
105
|
+
def translate(x, y, z = 0)
|
106
|
+
matrix__.translate x, y, z
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def rotate(angle)
|
111
|
+
matrix__.rotate @context.toDegrees__(angle)
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def scale(x, y, z = 1)
|
116
|
+
matrix__.scale x, y, z
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def resetMatrix()
|
121
|
+
@matrix = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def rotateX = nil
|
125
|
+
def rotateY = nil
|
126
|
+
def rotateZ = nil
|
127
|
+
|
128
|
+
# @private
|
129
|
+
def matrix__()
|
130
|
+
@matrix ||= Rays::Matrix.new
|
131
|
+
end
|
132
|
+
|
133
|
+
# @private
|
134
|
+
def getInternal__()
|
135
|
+
unless @polygon
|
136
|
+
return nil unless @points && @closed != nil
|
137
|
+
@polygon = self.class.createPolygon__ @mode, @points, @closed
|
138
|
+
end
|
139
|
+
@polygon
|
140
|
+
end
|
141
|
+
|
142
|
+
# @private
|
143
|
+
def draw__(painter, x, y, w = nil, h = nil)
|
144
|
+
poly = getInternal__
|
145
|
+
|
146
|
+
backup = nil
|
147
|
+
if @matrix && (poly || @children)
|
148
|
+
backup = painter.matrix
|
149
|
+
painter.matrix = backup * @matrix
|
150
|
+
end
|
151
|
+
|
152
|
+
if poly
|
153
|
+
if w || h
|
154
|
+
painter.polygon poly, x, y, w,h
|
155
|
+
else
|
156
|
+
painter.polygon poly, x, y
|
157
|
+
end
|
158
|
+
end
|
159
|
+
@children&.each {|o| o.draw__ painter, x, y, w, h}
|
160
|
+
|
161
|
+
painter.matrix = backup if backup
|
162
|
+
end
|
163
|
+
|
164
|
+
# @private
|
165
|
+
def self.createPolygon__(mode, points, close = false)
|
166
|
+
g = GraphicsContext
|
167
|
+
case mode
|
168
|
+
when g::POINTS then Rays::Polygon.points( *points)
|
169
|
+
when g::LINES then Rays::Polygon.lines( *points)
|
170
|
+
when g::TRIANGLES then Rays::Polygon.triangles( *points)
|
171
|
+
when g::TRIANGLE_FAN then Rays::Polygon.triangle_fan( *points)
|
172
|
+
when g::TRIANGLE_STRIP then Rays::Polygon.triangle_strip(*points)
|
173
|
+
when g::QUADS then Rays::Polygon.quads( *points)
|
174
|
+
when g::QUAD_STRIP then Rays::Polygon.quad_strip( *points)
|
175
|
+
when g::TESS, nil then Rays::Polygon.new(*points, loop: close)
|
176
|
+
else raise ArgumentError, "invalid polygon mode '#{mode}'"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end# Shape
|
181
|
+
|
182
|
+
|
183
|
+
end# Processing
|
data/lib/processing/vector.rb
CHANGED
@@ -463,9 +463,9 @@ module Processing
|
|
463
463
|
# @return [Vector] rotated this object
|
464
464
|
#
|
465
465
|
def rotate(angle)
|
466
|
-
|
467
|
-
@context.
|
468
|
-
@point.rotate!
|
466
|
+
deg = @context ?
|
467
|
+
@context.toDegrees__(angle) : angle * GraphicsContext::RAD2DEG__
|
468
|
+
@point.rotate! deg
|
469
469
|
self
|
470
470
|
end
|
471
471
|
|
data/processing.gemspec
CHANGED
@@ -25,14 +25,10 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.platform = Gem::Platform::RUBY
|
26
26
|
s.required_ruby_version = '>= 3.0.0'
|
27
27
|
|
28
|
-
s.add_runtime_dependency 'xot', '~> 0.1.
|
29
|
-
s.add_runtime_dependency 'rucy', '~> 0.1.
|
30
|
-
s.add_runtime_dependency 'rays', '~> 0.1.
|
31
|
-
s.add_runtime_dependency 'reflexion', '~> 0.1.
|
32
|
-
|
33
|
-
s.add_development_dependency 'rake'
|
34
|
-
s.add_development_dependency 'test-unit'
|
35
|
-
s.add_development_dependency 'yard'
|
28
|
+
s.add_runtime_dependency 'xot', '~> 0.1.41'
|
29
|
+
s.add_runtime_dependency 'rucy', '~> 0.1.42'
|
30
|
+
s.add_runtime_dependency 'rays', '~> 0.1.47'
|
31
|
+
s.add_runtime_dependency 'reflexion', '~> 0.1.55'
|
36
32
|
|
37
33
|
s.files = `git ls-files`.split $/
|
38
34
|
s.test_files = s.files.grep %r{^(test|spec|features)/}
|
data/test/helper.rb
CHANGED
@@ -5,38 +5,133 @@
|
|
5
5
|
require 'xot/test'
|
6
6
|
require 'processing/all'
|
7
7
|
|
8
|
+
require 'digest/md5'
|
9
|
+
require 'fileutils'
|
8
10
|
require 'tempfile'
|
9
11
|
require 'test/unit'
|
10
12
|
|
11
13
|
include Xot::Test
|
12
14
|
|
13
15
|
|
14
|
-
|
16
|
+
DEFAULT_DRAW_HEADER = <<~END
|
17
|
+
background 100
|
18
|
+
fill 255, 0, 0
|
19
|
+
stroke 0, 255, 0
|
20
|
+
strokeWeight 50
|
21
|
+
END
|
22
|
+
|
23
|
+
|
24
|
+
def test_with_p5?()
|
25
|
+
ENV['TEST_WITH_P5'] == '1'
|
26
|
+
end
|
27
|
+
|
28
|
+
def md5(s)
|
29
|
+
Digest::MD5.hexdigest s
|
30
|
+
end
|
31
|
+
|
32
|
+
def mkdir(dir: nil, filename: nil)
|
33
|
+
path = dir || File.dirname(filename)
|
34
|
+
FileUtils.mkdir_p path unless File.exist? path
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_label(index = 1)
|
38
|
+
caller_locations[index].then {|loc| "#{loc.label}_#{loc.lineno}"}
|
39
|
+
end
|
40
|
+
|
41
|
+
def temp_path(ext: nil, &block)
|
15
42
|
f = Tempfile.new
|
16
43
|
path = f.path
|
17
|
-
path +=
|
44
|
+
path += ext if ext
|
18
45
|
f.close!
|
19
46
|
block.call path
|
20
47
|
File.delete path
|
21
48
|
end
|
22
49
|
|
50
|
+
def draw_output_path(label, *sources, ext: '.png', dir: ext)
|
51
|
+
src = sources.compact.then {|ary| ary.empty? ? '' : "_#{md5 ary.join("\n")}"}
|
52
|
+
path = File.join __dir__, dir, label + src + ext
|
53
|
+
mkdir filename: path
|
54
|
+
path
|
55
|
+
end
|
56
|
+
|
23
57
|
def get_pixels(image)
|
24
58
|
%i[@image @image__]
|
25
59
|
.map {image.instance_variable_get _1}
|
26
60
|
.compact
|
27
61
|
.first
|
28
62
|
.bitmap
|
29
|
-
.
|
63
|
+
.pixels
|
30
64
|
end
|
31
65
|
|
32
|
-
def graphics(width = 10, height = 10, &block)
|
33
|
-
Processing::Graphics.new(width, height).tap do |g|
|
66
|
+
def graphics(width = 10, height = 10, *args, &block)
|
67
|
+
Processing::Graphics.new(width, height, *args).tap do |g|
|
34
68
|
g.beginDraw {block.call g, g.getInternal__} if block
|
35
69
|
end
|
36
70
|
end
|
37
71
|
|
72
|
+
def test_draw(*sources, width: 1000, height: 1000, pixelDensity: 1, label: nil)
|
73
|
+
graphics(width, height, pixelDensity).tap do |g|
|
74
|
+
g.beginDraw {g.instance_eval sources.compact.join("\n")}
|
75
|
+
g.save draw_output_path(label, *sources) if label
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
38
80
|
def assert_equal_vector(v1, v2, delta = 0.000001)
|
39
81
|
assert_in_delta v1.x, v2.x, delta
|
40
82
|
assert_in_delta v1.y, v2.y, delta
|
41
83
|
assert_in_delta v1.z, v2.z, delta
|
42
84
|
end
|
85
|
+
|
86
|
+
def assert_equal_pixels(expected, actual, threshold: 1.0)
|
87
|
+
exp_pixels = get_pixels expected
|
88
|
+
act_pixels = get_pixels actual
|
89
|
+
raise "Number of pixels does not match" if act_pixels.size != exp_pixels.size
|
90
|
+
|
91
|
+
equal_count = exp_pixels.zip(act_pixels).count {|a, b| a == b}
|
92
|
+
equal_rate = equal_count.to_f / act_pixels.size.to_f
|
93
|
+
assert equal_rate >= threshold, <<~EOS
|
94
|
+
The rate of the same pixel #{equal_rate} is below the threshold #{threshold}
|
95
|
+
EOS
|
96
|
+
end
|
97
|
+
|
98
|
+
def assert_equal_draw(
|
99
|
+
*shared_header, expected, actual, default_header: DEFAULT_DRAW_HEADER,
|
100
|
+
width: 1000, height: 1000, threshold: 1.0, label: test_label)
|
101
|
+
|
102
|
+
e = test_draw default_header, *shared_header, expected, label: "#{label}_expected"
|
103
|
+
a = test_draw default_header, *shared_header, actual, label: "#{label}_actual"
|
104
|
+
|
105
|
+
assert_equal_pixels e, a, threshold: threshold
|
106
|
+
end
|
107
|
+
|
108
|
+
def assert_p5_draw(
|
109
|
+
*sources, default_header: DEFAULT_DRAW_HEADER,
|
110
|
+
width: 1000, height: 1000, threshold: 0.99, label: test_label)
|
111
|
+
|
112
|
+
return unless test_with_p5?
|
113
|
+
|
114
|
+
source = [default_header, *sources].compact.join("\n")
|
115
|
+
path = draw_output_path "#{label}_expected", source
|
116
|
+
|
117
|
+
pd = draw_p5rb width, height, source, path, headless: true
|
118
|
+
actual = test_draw source, width: width, height: height, pixelDensity: pd
|
119
|
+
actual.save path.sub('_expected', '_actual')
|
120
|
+
|
121
|
+
assert_equal_pixels actual.loadImage(path), actual, threshold: threshold
|
122
|
+
end
|
123
|
+
|
124
|
+
def assert_p5_fill(*sources, **kwargs)
|
125
|
+
assert_p5_draw 'noStroke', *sources, label: test_label, **kwargs
|
126
|
+
end
|
127
|
+
|
128
|
+
def assert_p5_stroke(*sources, **kwargs)
|
129
|
+
assert_p5_draw 'noFill; stroke 0, 255, 0', *sources, label: test_label, **kwargs
|
130
|
+
end
|
131
|
+
|
132
|
+
def assert_p5_fill_stroke(*sources, **kwargs)
|
133
|
+
assert_p5_draw 'stroke 0, 255, 0', *sources, label: test_label, **kwargs
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
require_relative 'p5' if test_with_p5?
|
data/test/p5.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'ferrum'
|
3
|
+
|
4
|
+
RUBY_URL = 'https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@next/dist/browser.script.iife.js'
|
5
|
+
P5JS_URL = 'https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js'
|
6
|
+
P5RB_URL = 'https://raw.githubusercontent.com/ongaeshi/p5rb/master/docs/lib/p5.rb'
|
7
|
+
|
8
|
+
P5RB_SRC = URI.open(P5RB_URL) {|f| f.read}
|
9
|
+
|
10
|
+
def browser(width, height, headless: true)
|
11
|
+
hash = ($browsers ||= {})
|
12
|
+
key = [width, height, headless]
|
13
|
+
hash[key] ||= Ferrum::Browser.new headless: headless, window_size: [width, height]
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_p5rb_html(width, height, draw_src)
|
17
|
+
<<~END
|
18
|
+
<html>
|
19
|
+
<head>
|
20
|
+
<script src="#{RUBY_URL}"></script>
|
21
|
+
<script src="#{P5JS_URL}"></script>
|
22
|
+
<script type="text/ruby">#{P5RB_SRC}</script>
|
23
|
+
<style type="text/css">
|
24
|
+
body {margin: 0;}
|
25
|
+
</style>
|
26
|
+
<script type="text/javascript">
|
27
|
+
function completed() {
|
28
|
+
id = 'completed'
|
29
|
+
if (document.querySelector("#" + id)) return;
|
30
|
+
let e = document.createElement("span");
|
31
|
+
e.id = id;
|
32
|
+
document.body.appendChild(e);
|
33
|
+
}
|
34
|
+
</script>
|
35
|
+
<script type="text/ruby">
|
36
|
+
def setup()
|
37
|
+
createCanvas #{width}, #{height}
|
38
|
+
end
|
39
|
+
def draw()
|
40
|
+
#{draw_src}
|
41
|
+
JS.global.completed
|
42
|
+
end
|
43
|
+
P5::init
|
44
|
+
</script>
|
45
|
+
</head>
|
46
|
+
<body><main></main></body>
|
47
|
+
</html>
|
48
|
+
END
|
49
|
+
end
|
50
|
+
|
51
|
+
def sleep_until (try: 3, timeout: 10, &block)
|
52
|
+
now = -> offset = 0 {Time.now.to_f + offset}
|
53
|
+
limit = now[timeout]
|
54
|
+
until block.call
|
55
|
+
if now[] > limit
|
56
|
+
limit = now[timeout]
|
57
|
+
try -= 1
|
58
|
+
next if try > 0
|
59
|
+
raise 'Drawing timed out in p5.rb'
|
60
|
+
end
|
61
|
+
sleep 0.1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def draw_p5rb(width, height, draw_src, path, headless: true)
|
66
|
+
b = browser width, height, headless: headless
|
67
|
+
unless File.exist? path
|
68
|
+
b.reset
|
69
|
+
b.main_frame.content = get_p5rb_html width, height, draw_src
|
70
|
+
sleep_until do
|
71
|
+
b.evaluate 'document.querySelector("#completed") != null'
|
72
|
+
end
|
73
|
+
b.screenshot path: path
|
74
|
+
end
|
75
|
+
b.device_pixel_ratio
|
76
|
+
end
|
data/test/test_graphics.rb
CHANGED
@@ -3,12 +3,6 @@ require_relative 'helper'
|
|
3
3
|
|
4
4
|
class TestGraphics < Test::Unit::TestCase
|
5
5
|
|
6
|
-
P = Processing
|
7
|
-
|
8
|
-
def graphics(w = 10, h = 10)
|
9
|
-
P::Graphics.new w, h
|
10
|
-
end
|
11
|
-
|
12
6
|
def test_beginDraw()
|
13
7
|
g = graphics
|
14
8
|
g.beginDraw
|
@@ -24,9 +18,9 @@ class TestGraphics < Test::Unit::TestCase
|
|
24
18
|
g.ellipseMode :corner
|
25
19
|
g.ellipse 0, 0, g.width, g.height
|
26
20
|
end
|
27
|
-
|
21
|
+
temp_path(ext: '.png') do |path|
|
28
22
|
assert_nothing_raised {g.save path}
|
29
|
-
|
23
|
+
assert_equal_pixels g, g.loadImage(path)
|
30
24
|
end
|
31
25
|
end
|
32
26
|
|