processing 0.5.29 → 0.5.31
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 +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
|
|