Skip to content

Commit a2ba74a

Browse files
committed
circumcircle
1 parent 3921ac1 commit a2ba74a

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Loosely based on a sketch by Bárbara Almeida
2+
# https://www.openprocessing.org/sketch/179567
3+
# Here is a sketch that clearly demonstrates some of the most egregious parts of
4+
# vanilla processing:-
5+
# PVector is over complicated and overloads 2D and 3D functionality, cf Vec2D
6+
# JRubyArt and propane which behaves as 2D vector (cross product is a float) and
7+
# can be used in chain calculations.
8+
# The inverted Y axis in processing requires remapping to use in graphs etc.
9+
# The map method of processing can be used to this remapping, however in many
10+
# languages (python, ruby etc) map is a keyword with alternative usage.
11+
# For this reason in JRubyArt and propane we have replaced vanilla processing
12+
# map with map1d.
13+
14+
##################################################
15+
# Usage click on screen with mouse to generate a point, repeat as required
16+
# to create a triangle, and the circumcircle will be drawn unless the points are
17+
# collinear. Additional clicks erase the first point and add a new point. To
18+
# demonstrate collinear test press 'c' or 'C', press space-bar to clear
19+
##################################################
20+
21+
load_library :circle
22+
23+
attr_reader :point, :font, :points, :circle, :center
24+
25+
def settings
26+
size 800, 800
27+
# pixel_density(2) # for HiDpi screens
28+
# smooth # see https://processing.org/reference/smooth_.html
29+
end
30+
31+
def setup
32+
sketch_title 'Circumcircle Through Three Points'
33+
@point = MathPoint.new
34+
@font = create_font('Arial', 16, true)
35+
@points = TPoints.new
36+
ellipse_mode RADIUS
37+
end
38+
39+
def draw
40+
graph_paper
41+
point.display
42+
points.map(&:display)
43+
return unless circle
44+
center.display
45+
no_fill
46+
stroke 200
47+
triangle(
48+
points[0].screen_x,
49+
points[0].screen_y,
50+
points[1].screen_x,
51+
points[1].screen_y,
52+
points[2].screen_x,
53+
points[2].screen_y
54+
)
55+
ellipse(center.screen_x, center.screen_y, circle.radius, circle.radius)
56+
end
57+
58+
def mouse_pressed
59+
points << MathPoint.new.from_sketch(mouse_x, mouse_y)
60+
return unless points.full?
61+
@circle = Circumcircle.new(points.array)
62+
circle.calculate
63+
@center = MathPoint.new(circle.center)
64+
end
65+
66+
def key_pressed
67+
case key
68+
when ' '
69+
points.clear
70+
when 'c', 'C'
71+
points << MathPoint.new(Vec2D.new(0, 0))
72+
points << MathPoint.new(Vec2D.new(100, 100))
73+
points << MathPoint.new(Vec2D.new(300, 300))
74+
puts 'collinear points' if points.collinear?
75+
end
76+
end
77+
78+
def graph_paper
79+
background(0)
80+
cell_size = 20 # size of the grid
81+
dim = cell_size * 2
82+
(dim..width - dim).step(cell_size) do |i|
83+
stroke(75, 135, 155)
84+
line(i, dim, i, height - dim)
85+
end
86+
(dim..height - dim).step(cell_size) do |i|
87+
line(dim, i, width - dim, i)
88+
end
89+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require_relative 'lib/circumcircle'
2+
require_relative 'lib/math_point'
3+
require_relative 'lib/points'
4+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
PBisector = Struct.new(:vector, :angle) # perpendicular bisector
3+
Vect = Struct.new(:x, :y, :z) # for calculation of center
4+
# Circumcircle from 3 points
5+
class Circumcircle
6+
include Math
7+
attr_reader :center, :radius, :points
8+
def initialize(points)
9+
@points = points
10+
end
11+
12+
def calculate
13+
ab = bisector(points[0], points[1]) # find 2 midpoints
14+
bc = bisector(points[1], points[2])
15+
@center = circumcenter(ab, bc)
16+
@radius = center.dist(points[2]) # points[2] = c
17+
end
18+
19+
def bisector(a, b)
20+
midpoint = (a + b) / 2.0 # middle of ab (or bc)
21+
theta = atan2(b.y - a.y, b.x - a.x) # slope of ab (or bc)
22+
PBisector.new(midpoint, theta - PI / 2)
23+
end
24+
25+
def circumcenter(pb1, pb2)
26+
# equation of the first bisector (ax - y = -b)
27+
a0 = tan pb1.angle
28+
v0 = pb1.vector
29+
a1 = tan pb2.angle
30+
v1 = pb2.vector
31+
eq0 = Vect.new(a0, -1, -1 * (v0.y - v0.x * a0))
32+
eq1 = Vect.new(a1, -1, -1 * (v1.y - v1.x * a1))
33+
# calculate x and y coordinates of the circumcenter
34+
ox = (eq1.y * eq0.z - eq0.y * eq1.z) /
35+
(eq0.x * eq1.y - eq1.x * eq0.y)
36+
oy = (eq0.x * eq1.z - eq1.x * eq0.z) /
37+
(eq0.x * eq1.y - eq1.x * eq0.y)
38+
Vec2D.new(ox, oy)
39+
end
40+
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
# In processing the Y-axis is inverted, plus we have a fake origin for display
3+
# pos is the position in the Math World, which we need to translate to sketch
4+
class MathPoint
5+
include Processing::Proxy # to access sketch methods, including height
6+
OR = 40.0
7+
FORM = '(%d, %d)'.freeze
8+
attr_reader :pos, :from_mouse
9+
def initialize(pos = Vec2D.new)
10+
@pos = pos
11+
@from_mouse = false
12+
end
13+
14+
def from_sketch(x, y)
15+
@from_mouse = true
16+
@pos = Vec2D.new(x - OR, map1d(y, height - OR..-OR, 0..height))
17+
self
18+
end
19+
20+
def xpos
21+
pos.x
22+
end
23+
24+
def ypos
25+
pos.y
26+
end
27+
28+
def screen_x
29+
pos.x + OR
30+
end
31+
32+
def screen_y
33+
map1d(pos.y, 0..height, height - OR..-OR)
34+
end
35+
36+
def display
37+
fill 200
38+
no_stroke
39+
ellipse(screen_x, screen_y, 2.5, 2.5)
40+
display_text
41+
return unless from_mouse
42+
no_fill
43+
stroke(200, 0, 0)
44+
ellipse(screen_x, screen_y, 5, 5)
45+
end
46+
47+
private
48+
49+
def display_text
50+
from_mouse ? fill(255, 255, 0) : fill(0, 255, 0)
51+
text_font(font, 12)
52+
text(format(FORM, xpos.round, ypos.round), screen_x + 5, screen_y - 5)
53+
end
54+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
require 'forwardable'
3+
MAX_POINT = 3
4+
# A collection of a maximum of 3 points in a Math World
5+
# includes a collinearity test, and a map to simple Vec2D array
6+
class TPoints
7+
extend Forwardable
8+
def_delegators(:@points, :each, :map, :size, :shift, :clear, :[])
9+
include Enumerable
10+
11+
attr_reader :points
12+
13+
def initialize
14+
@points = []
15+
end
16+
17+
def <<(pt)
18+
points << pt
19+
shift if size > MAX_POINT
20+
end
21+
22+
def collinear?
23+
return false unless full?
24+
(array[0] - array[1]).cross(array[1] - array[2]).zero?
25+
end
26+
27+
def array
28+
points.map(&:pos)
29+
end
30+
31+
def full?
32+
points.length == MAX_POINT
33+
end
34+
end

0 commit comments

Comments
 (0)